;
8 | let lastSentFormData: FormData;
9 | let lastEventListener: jest.EmptyFunction;
10 |
11 | beforeEach(() => {
12 | xmlHttpReq = global.XMLHttpRequest = jest.fn(() => ({
13 | addEventListener: jest.fn((event: string, onload: jest.EmptyFunction) => {
14 | if (event === 'load')
15 | lastEventListener = onload;
16 | }),
17 | open: jest.fn(),
18 | send: jest.fn((data: FormData) => {
19 | lastSentFormData = data;
20 | if (lastEventListener) {
21 | lastEventListener.call(this, {
22 | target: {
23 | responseText: JSON.stringify({ foo: 'bar' }),
24 | },
25 | });
26 | }
27 | }),
28 | setRequestHeader: jest.fn(),
29 | }));
30 | });
31 |
32 | afterEach(() => {
33 | xmlHttpReq = null;
34 | lastSentFormData = null;
35 | lastEventListener = null;
36 | });
37 |
38 | test('Static instance is defined', () => {
39 | expect(WordPressRestApi.instance).toBeInstanceOf(WordPressRestApi);
40 | });
41 |
42 | test('Can make a GET request to the plugin REST API', () => {
43 | const wordPressRestApi: WordPressRestApi = new WordPressRestApi();
44 | const onLoad = jest.fn();
45 |
46 | const request = wordPressRestApi.pluginRestGet('settings', onLoad);
47 |
48 | expect(request.open).toHaveBeenCalledWith('GET', `${REST_OPTIONS.base}${REST_OPTIONS.disqusBase}settings`);
49 | expect(request.setRequestHeader).toHaveBeenCalledWith('X-WP-Nonce', REST_OPTIONS.nonce);
50 | expect(request.setRequestHeader).toHaveBeenCalledWith('Content-type', 'application/json');
51 | expect(request.addEventListener).toHaveBeenCalledTimes(1);
52 | expect(request.send).toHaveBeenCalledWith(null);
53 | expect(onLoad).toHaveBeenCalled();
54 | });
55 |
56 | test('Can make a POST request to the plugin REST API', () => {
57 | const wordPressRestApi: WordPressRestApi = new WordPressRestApi();
58 | const onLoad = jest.fn();
59 | const postData = { foo: 'bar' };
60 | const request = wordPressRestApi.pluginRestPost('settings', postData, onLoad);
61 |
62 | expect(request.open).toHaveBeenCalledWith('POST', `${REST_OPTIONS.base}${REST_OPTIONS.disqusBase}settings`);
63 | expect(request.setRequestHeader).toHaveBeenCalledWith('X-WP-Nonce', REST_OPTIONS.nonce);
64 | expect(request.setRequestHeader).toHaveBeenCalledWith('Content-type', 'application/json');
65 | expect(request.addEventListener).toHaveBeenCalledTimes(1);
66 | expect(request.send).toHaveBeenCalledWith(JSON.stringify(postData));
67 | expect(onLoad).toHaveBeenCalled();
68 | });
69 |
70 | test('Can make a GET request to the WordPress REST API', () => {
71 | const wordPressRestApi: WordPressRestApi = new WordPressRestApi();
72 | const onLoad = jest.fn();
73 |
74 | const request = wordPressRestApi.wordpressRestGet('comments', 'foo=bar', onLoad);
75 |
76 | expect(request.open).toHaveBeenCalledWith('GET', `${REST_OPTIONS.base}wp/v2/comments?foo=bar`);
77 | expect(request.setRequestHeader).toHaveBeenCalledWith('X-WP-Nonce', REST_OPTIONS.nonce);
78 | expect(request.setRequestHeader).toHaveBeenCalledWith('Content-type', 'application/json');
79 | expect(request.addEventListener).toHaveBeenCalledTimes(1);
80 | expect(request.send).toHaveBeenCalledWith(null);
81 | expect(onLoad).toHaveBeenCalled();
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/frontend/tests/actions.test.ts:
--------------------------------------------------------------------------------
1 | import * as actions from '../src/ts/actions';
2 | import { ExportLogStaus, IExportPostLog, IMessage, InstallationState } from '../src/ts/reducers/AdminState';
3 |
4 | test('updateAdminOptionsAction creates an action to update the admin options', () => {
5 | const newAdminOptions: any = {
6 | test_option: true,
7 | };
8 |
9 | const expectedAction: any = {
10 | data: {
11 | test_option: true,
12 | },
13 | type: 'UPDATE_ADMIN_OPTIONS',
14 | };
15 |
16 | expect(actions.updateAdminOptionsAction(newAdminOptions)).toEqual(expectedAction);
17 | });
18 |
19 | test('updateLocalOptionAction creates an action to update the local options', () => {
20 | const expectedAction: any = {
21 | data: {
22 | test_option: 'foo',
23 | },
24 | type: 'UPDATE_LOCAL_OPTION',
25 | };
26 |
27 | expect(actions.updateLocalOptionAction('test_option', 'foo')).toEqual(expectedAction);
28 | });
29 |
30 | test('updateSyncStatusAction creates an action to update the sync status', () => {
31 | const newSyncStatus: any = {
32 | sync_status: 'sync',
33 | };
34 |
35 | const expectedAction: any = {
36 | data: {
37 | sync_status: 'sync',
38 | },
39 | type: 'UPDATE_SYNC_STATUS',
40 | };
41 |
42 | expect(actions.updateSyncStatusAction(newSyncStatus)).toEqual(expectedAction);
43 | });
44 |
45 | test('toggleValueAction creates an action to toggle a value', () => {
46 | const expectedAction: any = {
47 | data: 'foo',
48 | type: 'TOGGLE_VALUE',
49 | };
50 |
51 | expect(actions.toggleValueAction('foo')).toEqual(expectedAction);
52 | });
53 |
54 | test('setValueAction creates an action to set a value', () => {
55 | const expectedAction: any = {
56 | data: {
57 | test: 'foo',
58 | },
59 | type: 'SET_VALUE',
60 | };
61 |
62 | expect(actions.setValueAction('test', 'foo')).toEqual(expectedAction);
63 | });
64 |
65 | test('setMessageAction creates an action to update the message', () => {
66 | const message: IMessage = {
67 | text: 'Test message',
68 | type: 'error',
69 | };
70 |
71 | const expectedAction: any = {
72 | data: {
73 | text: 'Test message',
74 | type: 'error',
75 | },
76 | type: 'SET_MESSAGE',
77 | };
78 |
79 | expect(actions.setMessageAction(message)).toEqual(expectedAction);
80 | });
81 |
82 | test('changeInstallStateAction creates an action to update the install state', () => {
83 | const state: InstallationState = InstallationState.none;
84 |
85 | const expectedAction: any = {
86 | data: InstallationState.none,
87 | type: 'CHANGE_INSTALL_STATE',
88 | };
89 |
90 | expect(actions.changeInstallStateAction(state)).toEqual(expectedAction);
91 | });
92 |
93 | test('changeTabStateAction creates an action to change a tab state', () => {
94 | const expectedAction: any = {
95 | data: 'footab',
96 | type: 'CHANGE_TAB_STATE',
97 | };
98 |
99 | expect(actions.changeTabStateAction('footab')).toEqual(expectedAction);
100 | });
101 |
102 | test('updateExportPostLogAction creates an action to change update a post log', () => {
103 | const postLog: IExportPostLog = {
104 | id: 1,
105 | link: 'https://foo.com',
106 | status: ExportLogStaus.pending,
107 | title: 'Foo',
108 | };
109 |
110 | const expectedAction: any = {
111 | data: {
112 | id: 1,
113 | link: 'https://foo.com',
114 | status: ExportLogStaus.pending,
115 | title: 'Foo',
116 | },
117 | type: 'UPDATE_EXPORT_POST_LOG',
118 | };
119 |
120 | expect(actions.updateExportPostLogAction(postLog)).toEqual(expectedAction);
121 | });
122 |
--------------------------------------------------------------------------------
/frontend/tests/beforeAll.ts:
--------------------------------------------------------------------------------
1 | import { getDefaultAdminConfig } from './fixtures';
2 |
3 | declare var global: any
4 |
5 | global.__ = (str: string) => str;
6 | global.DISQUS_WP = getDefaultAdminConfig();
7 |
--------------------------------------------------------------------------------
/frontend/tests/components/AdvancedConfigForm.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import AdvancedConfigForm from '../../src/ts/components/AdvancedConfigForm';
4 | import { IFormProps } from '../../src/ts/components/FormProps';
5 | import { getDefaultFormProps } from '../fixtures';
6 |
7 | describe('AdvancedConfigForm component', () => {
8 | test('Renders basic form', () => {
9 | const props: IFormProps = getDefaultFormProps();
10 | const component = TestRenderer.create();
11 |
12 | expect(component.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | test('Renders form with values', () => {
16 | const props: IFormProps = getDefaultFormProps();
17 | props.data = props.data.with({
18 | localAdminOptions: props.data.localAdminOptions.with({
19 | disqus_render_js: true,
20 | }),
21 | });
22 | const component = TestRenderer.create();
23 |
24 | expect(component.toJSON()).toMatchSnapshot();
25 | });
26 |
27 | test('onInputChange prop gets called when values change', () => {
28 | const props: IFormProps = getDefaultFormProps();
29 | const onInputChange = props.onInputChange = jest.fn();
30 | props.data = props.data.with({
31 | localAdminOptions: props.data.localAdminOptions.with({
32 | disqus_render_js: true,
33 | }),
34 | });
35 | const component = TestRenderer.create();
36 | const root = component.root;
37 |
38 | root.findAllByType('input').forEach((input: any) => {
39 | if (input.props.type !== 'submit') {
40 | input.props.onChange.call(null, { foo: 'bar' });
41 | expect(onInputChange).toHaveBeenLastCalledWith(input.props.id, { foo: 'bar' });
42 | }
43 | });
44 | });
45 |
46 | test('Submitting the form calls onSubmitSiteForm prop', () => {
47 | const props: IFormProps = getDefaultFormProps();
48 | const onSubmitSiteForm = props.onSubmitSiteForm = jest.fn();
49 | props.data = props.data.with({
50 | localAdminOptions: props.data.localAdminOptions.with({
51 | disqus_render_js: true,
52 | }),
53 | });
54 | const component = TestRenderer.create();
55 | const root = component.root;
56 |
57 | const form = root.findByType('form');
58 | form.props.onSubmit();
59 |
60 | expect(onSubmitSiteForm).toHaveBeenCalled();
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/frontend/tests/components/ExportComments.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import ExportComments from '../../src/ts/components/ExportComments';
4 | import { IFormProps } from '../../src/ts/components/FormProps';
5 | import { ExportLogStaus, IExportPostLog } from '../../src/ts/reducers/AdminState';
6 | import { getDefaultFormProps } from '../fixtures';
7 |
8 | describe('ExportComments component', () => {
9 | test('Renders basic form without any export logs', () => {
10 | const props: IFormProps = getDefaultFormProps();
11 | const component = TestRenderer.create();
12 |
13 | expect(component.toJSON()).toMatchSnapshot();
14 | });
15 |
16 | test('Disables submit button during export', () => {
17 | const props: IFormProps = getDefaultFormProps();
18 | props.data = props.data.set('isExportRunning', true);
19 |
20 | const component = TestRenderer.create();
21 |
22 | expect(component.toJSON()).toMatchSnapshot();
23 | });
24 |
25 | test('Renders export logs', () => {
26 | const props: IFormProps = getDefaultFormProps();
27 | const exportLog: IExportPostLog = {
28 | id: 1,
29 | link: 'https://foo.com?p=1',
30 | status: ExportLogStaus.pending,
31 | title: 'Foo',
32 | };
33 | props.data = props.data.with({
34 | exportLogs: props.data.exportLogs.set(exportLog.id, exportLog),
35 | isExportRunning: true,
36 | });
37 |
38 | const component = TestRenderer.create();
39 |
40 | expect(component.toJSON()).toMatchSnapshot();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/frontend/tests/components/HelpResources.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import HelpResources from '../../src/ts/components/HelpResources';
4 |
5 | test('SupportLinks renders with correct links', () => {
6 | const component = TestRenderer.create();
7 |
8 | expect(component).toMatchSnapshot();
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/tests/components/Install.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import { IFormProps } from '../../src/ts/components/FormProps';
4 | import Install from '../../src/ts/components/Install';
5 | import { InstallationState } from '../../src/ts/reducers/AdminState';
6 | import { getDefaultFormProps } from '../fixtures';
7 |
8 | describe('Install component rendering', () => {
9 | test('InstallationState.none displays the entry form snapshot', () => {
10 | const props: IFormProps = getDefaultFormProps();
11 | props.data = props.data.set('installationState', InstallationState.none);
12 | const component = TestRenderer.create();
13 |
14 | expect(component.toJSON()).toMatchSnapshot();
15 | });
16 |
17 | test('InstallationState.hasAccount displays a followup question about registering a site', () => {
18 | const props: IFormProps = getDefaultFormProps();
19 | props.data = props.data.set('installationState', InstallationState.hasAccount);
20 | const component = TestRenderer.create();
21 |
22 | expect(component.toJSON()).toMatchSnapshot();
23 | });
24 |
25 | test('InstallationState.noAccount displays a instructions for registering an account', () => {
26 | const props: IFormProps = getDefaultFormProps();
27 | props.data = props.data.set('installationState', InstallationState.noAccount);
28 | const component = TestRenderer.create();
29 |
30 | expect(component.toJSON()).toMatchSnapshot();
31 | });
32 |
33 | test('InstallationState.hasSite displays a instructions for installing using an existing site', () => {
34 | const props: IFormProps = getDefaultFormProps();
35 | props.data = props.data.set('installationState', InstallationState.hasSite);
36 | const component = TestRenderer.create();
37 |
38 | expect(component.toJSON()).toMatchSnapshot();
39 | });
40 |
41 | test('InstallationState.noSite displays a instructions for installing using an new site', () => {
42 | const props: IFormProps = getDefaultFormProps();
43 | props.data = props.data.set('installationState', InstallationState.noSite);
44 | const component = TestRenderer.create();
45 |
46 | expect(component.toJSON()).toMatchSnapshot();
47 | });
48 |
49 | test('InstallationState.installed asks to install onto existing site', () => {
50 | const props: IFormProps = getDefaultFormProps();
51 | props.data = props.data.with({
52 | adminOptions: props.data.adminOptions.with({
53 | disqus_forum_url: 'foo',
54 | }),
55 | installationState: InstallationState.installed,
56 | localAdminOptions: props.data.localAdminOptions.with({
57 | disqus_forum_url: 'foo',
58 | }),
59 | });
60 | const component = TestRenderer.create();
61 |
62 | expect(component.toJSON()).toMatchSnapshot();
63 | });
64 |
65 | test('InstallationState.reinstallSite links to the site\'s installation page', () => {
66 | const props: IFormProps = getDefaultFormProps();
67 | props.data = props.data.with({
68 | adminOptions: props.data.adminOptions.with({
69 | disqus_forum_url: 'foo',
70 | }),
71 | installationState: InstallationState.reinstallSite,
72 | localAdminOptions: props.data.localAdminOptions.with({
73 | disqus_forum_url: 'foo',
74 | }),
75 | });
76 | const component = TestRenderer.create();
77 |
78 | expect(component.toJSON()).toMatchSnapshot();
79 | });
80 |
81 | test('Handles cross-site communication', () => {
82 | // tslint:disable:no-string-literal
83 | const mockLocationReload = jest.fn();
84 | const mockLocationObject = window.location.reload = mockLocationReload;
85 |
86 | const mockChildWindowPostMessage = jest.fn();
87 | const mockOpenFunction = window['open'] = jest.fn();
88 |
89 | mockOpenFunction.mockReturnValue({
90 | postMessage: mockChildWindowPostMessage,
91 | });
92 | let postMessageHandler = jest.fn();
93 | const mockEventListenerFunction = window['addEventListener'] = jest.fn(
94 | (type: string, listener: () => void, options: boolean) => {
95 | postMessageHandler = jest.fn(listener);
96 | },
97 | );
98 | // tslint:enable:no-string-literal
99 |
100 | const props: IFormProps = getDefaultFormProps();
101 | props.data = props.data.set('installationState', InstallationState.hasSite);
102 | props.data = props.data.set('adminOptions', props.data.adminOptions.set('disqus_sync_token', 'foo'));
103 | const component = TestRenderer.create();
104 |
105 | const openDisqusButton = component.root.findByProps({children: 'WordPress installation page'});
106 | openDisqusButton.props.onClick();
107 |
108 | // tslint:disable-next-line:max-line-length
109 | const expectedUrl = 'https://disqus.com/profile/login/?next=https%3A%2F%2Fdisqus.com%2Fadmin%2Finstall%2Fplatforms%2Fwordpress%2F';
110 |
111 | expect(mockOpenFunction).toBeCalledWith(expectedUrl);
112 | expect(mockEventListenerFunction).toBeCalledWith('message', expect.anything(), false);
113 |
114 | postMessageHandler({
115 | data: 'installPageReady',
116 | origin: 'https://foo.disqus.com/',
117 | });
118 | const expectedSyncToken = 'https://foo.com/wp-json/disqus/v1/settings foo';
119 |
120 | expect(mockChildWindowPostMessage).toBeCalledWith(expectedSyncToken, 'https://foo.disqus.com/');
121 |
122 | postMessageHandler({
123 | data: 'configurationUpdated',
124 | origin: 'https://foo.disqus.com/',
125 | });
126 |
127 | expect(mockLocationReload).toHaveBeenCalledTimes(1);
128 |
129 | postMessageHandler({
130 | data: 'configurationUpdated',
131 | origin: 'https://foo.com/',
132 | });
133 |
134 | expect(mockLocationReload).toHaveBeenCalledTimes(1);
135 | });
136 | });
137 |
--------------------------------------------------------------------------------
/frontend/tests/components/Message.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import Message from '../../src/ts/components/Message';
4 | import { IMessage } from '../../src/ts/reducers/AdminState';
5 |
6 | describe('Message', () => {
7 | const props: IMessage = {
8 | onDismiss: null,
9 | text: 'Test message',
10 | type: 'error',
11 | };
12 |
13 | test('Includes message content and class name', () => {
14 | const component = TestRenderer.create();
15 |
16 | expect(component).toMatchSnapshot();
17 | });
18 |
19 | test('Dismiss button is rendered with the onDismiss prop', () => {
20 | const onDismiss = jest.fn();
21 | const component = TestRenderer.create();
22 | const root = component.root;
23 | const dismissButton = root.findByType('button');
24 | dismissButton.props.onClick();
25 |
26 | expect(onDismiss).toHaveBeenCalled();
27 | });
28 |
29 | test('Excludes dismiss button is without the onDismiss prop', () => {
30 | const component = TestRenderer.create();
31 | const root = component.root;
32 |
33 | expect(() => {
34 | root.findByType('button');
35 | }).toThrow();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/frontend/tests/components/SSOConfigForm.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import { IFormProps } from '../../src/ts/components/FormProps';
4 | import SSOConfigForm from '../../src/ts/components/SSOConfigForm';
5 | import { getDefaultFormProps } from '../fixtures';
6 |
7 | describe('SSOConfigForm component', () => {
8 | test('Renders basic form', () => {
9 | const props: IFormProps = getDefaultFormProps();
10 | const component = TestRenderer.create();
11 |
12 | expect(component.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | test('Renders form with values', () => {
16 | const props: IFormProps = getDefaultFormProps();
17 | props.data = props.data.with({
18 | localAdminOptions: props.data.localAdminOptions.with({
19 | disqus_sso_button: 'https://foo.com/sso.png',
20 | disqus_sso_enabled: true,
21 | }),
22 | });
23 | const component = TestRenderer.create();
24 |
25 | expect(component.toJSON()).toMatchSnapshot();
26 | });
27 |
28 | test('onInputChange prop gets called when values change', () => {
29 | const props: IFormProps = getDefaultFormProps();
30 | const onInputChange = props.onInputChange = jest.fn();
31 | props.data = props.data.with({
32 | localAdminOptions: props.data.localAdminOptions.with({
33 | disqus_sso_button: 'https://foo.com/sso.png',
34 | disqus_sso_enabled: true,
35 | }),
36 | });
37 | const component = TestRenderer.create();
38 | const root = component.root;
39 |
40 | root.findAllByType('input').forEach((input: any) => {
41 | if (input.props.type !== 'submit') {
42 | input.props.onChange.call(null, { foo: 'bar' });
43 | expect(onInputChange).toHaveBeenLastCalledWith(input.props.id, { foo: 'bar' });
44 | }
45 | });
46 | });
47 |
48 | test('Submitting the form calls onSubmitSiteForm prop', () => {
49 | const props: IFormProps = getDefaultFormProps();
50 | const onSubmitSiteForm = props.onSubmitSiteForm = jest.fn();
51 | props.data = props.data.with({
52 | localAdminOptions: props.data.localAdminOptions.with({
53 | disqus_sso_button: 'https://foo.com/sso.png',
54 | disqus_sso_enabled: true,
55 | }),
56 | });
57 | const component = TestRenderer.create();
58 | const root = component.root;
59 |
60 | const form = root.findByType('form');
61 | form.props.onSubmit();
62 |
63 | expect(onSubmitSiteForm).toHaveBeenCalled();
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/frontend/tests/components/SiteConfigForm.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import { IFormProps } from '../../src/ts/components/FormProps';
4 | import SiteConfigForm from '../../src/ts/components/SiteConfigForm';
5 | import { getDefaultFormProps } from '../fixtures';
6 |
7 | describe('SiteConfigForm component', () => {
8 | test('Renders basic form', () => {
9 | const props: IFormProps = getDefaultFormProps();
10 | const component = TestRenderer.create();
11 |
12 | expect(component.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | test('Renders form with save button unlocked', () => {
16 | const props: IFormProps = getDefaultFormProps();
17 | props.data = props.data.set('isSiteFormLocked', false);
18 | const component = TestRenderer.create();
19 |
20 | expect(component.toJSON()).toMatchSnapshot();
21 | });
22 |
23 | test('Renders form with values', () => {
24 | const props: IFormProps = getDefaultFormProps();
25 | props.data = props.data.with({
26 | localAdminOptions: props.data.localAdminOptions.with({
27 | disqus_admin_access_token: 'qux',
28 | disqus_forum_url: 'foo',
29 | disqus_public_key: 'bar',
30 | disqus_secret_key: 'baz',
31 | }),
32 | });
33 | const component = TestRenderer.create();
34 |
35 | expect(component.toJSON()).toMatchSnapshot();
36 | });
37 |
38 | test('onInputChange prop gets called when values change', () => {
39 | const props: IFormProps = getDefaultFormProps();
40 | const onInputChange = props.onInputChange = jest.fn();
41 | props.data = props.data.with({
42 | localAdminOptions: props.data.localAdminOptions.with({
43 | disqus_admin_access_token: 'qux',
44 | disqus_forum_url: 'foo',
45 | disqus_public_key: 'bar',
46 | disqus_secret_key: 'baz',
47 | }),
48 | });
49 | const component = TestRenderer.create();
50 | const root = component.root;
51 |
52 | root.findAllByType('input').forEach((input: any) => {
53 | input.props.onChange.call(null, { foo: 'bar' });
54 | expect(onInputChange).toHaveBeenLastCalledWith(input.props.id, { foo: 'bar' });
55 | });
56 | });
57 |
58 | test('Submitting the form calls onSubmitSiteForm prop', () => {
59 | const props: IFormProps = getDefaultFormProps();
60 | const onSubmitSiteForm = props.onSubmitSiteForm = jest.fn();
61 | props.data = props.data.with({
62 | isSiteFormLocked: false,
63 | localAdminOptions: props.data.localAdminOptions.with({
64 | disqus_admin_access_token: 'qux',
65 | disqus_forum_url: 'foo',
66 | disqus_public_key: 'bar',
67 | disqus_secret_key: 'baz',
68 | }),
69 | });
70 | const component = TestRenderer.create();
71 | const root = component.root;
72 |
73 | const form = root.findByType('form');
74 | form.props.onSubmit();
75 |
76 | expect(onSubmitSiteForm).toHaveBeenCalled();
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/frontend/tests/components/SupportDiagnostics.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import SupportDiagnostics from '../../src/ts/components/SupportDiagnostics';
4 | import { getDefaultFormProps } from '../fixtures';
5 |
6 | test('SupportDiagnostics renders a readonly textarea with admin config data', () => {
7 | const component = TestRenderer.create();
8 | const root = component.root;
9 | const textarea = root.findByType('textarea');
10 |
11 | expect(textarea.props.readOnly).toBe(true);
12 | expect(textarea.props.value).toBe(JSON.stringify(getDefaultFormProps(), null, 4));
13 | });
14 |
--------------------------------------------------------------------------------
/frontend/tests/components/SupportLinks.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import SupportLinks from '../../src/ts/components/SupportLinks';
4 |
5 | test('SupportLinks renders with correct links', () => {
6 | const component = TestRenderer.create();
7 |
8 | expect(component).toMatchSnapshot();
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/tests/components/SyncConfigForm.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import { IFormProps } from '../../src/ts/components/FormProps';
4 | import SyncConfigForm from '../../src/ts/components/SyncConfigForm';
5 | import { getDefaultFormProps } from '../fixtures';
6 |
7 | describe('SyncConfigForm component', () => {
8 | test('Renders form with no sync enabled', () => {
9 | const props: IFormProps = getDefaultFormProps();
10 | const component = TestRenderer.create();
11 |
12 | expect(component.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | test('Renders form with disabled sync subscription', () => {
16 | const props: IFormProps = getDefaultFormProps();
17 | props.data = props.data.with({
18 | syncStatus: props.data.syncStatus.with({
19 | subscribed: false,
20 | }),
21 | });
22 | const component = TestRenderer.create();
23 |
24 | expect(component.toJSON()).toMatchSnapshot();
25 | });
26 |
27 | test('Renders form with paused sync subscription', () => {
28 | const props: IFormProps = getDefaultFormProps();
29 | props.data = props.data.with({
30 | syncStatus: props.data.syncStatus.with({
31 | requires_update: true,
32 | subscribed: true,
33 | }),
34 | });
35 | const component = TestRenderer.create();
36 |
37 | expect(component.toJSON()).toMatchSnapshot();
38 | });
39 |
40 | test('Renders form with enabled sync subscription', () => {
41 | const props: IFormProps = getDefaultFormProps();
42 | props.data = props.data.with({
43 | syncStatus: props.data.syncStatus.with({
44 | enabled: true,
45 | subscribed: true,
46 | }),
47 | });
48 | const component = TestRenderer.create();
49 |
50 | expect(component.toJSON()).toMatchSnapshot();
51 | });
52 |
53 | test('Submitting the form calls onSubmitSyncConfigForm prop', () => {
54 | const props: IFormProps = getDefaultFormProps();
55 | const onSubmitSyncConfigForm = props.onSubmitSyncConfigForm = jest.fn();
56 | props.data = props.data.with({
57 | syncStatus: props.data.syncStatus.with({
58 | subscribed: false,
59 | }),
60 | });
61 | const component = TestRenderer.create();
62 | const root = component.root;
63 |
64 | const form = root.findByType('form');
65 | form.props.onSubmit();
66 |
67 | expect(onSubmitSyncConfigForm).toHaveBeenCalled();
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/frontend/tests/components/SyncTokenForm.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import SyncTokenForm from '../../src/ts/components/SyncTokenForm';
4 | import { getDefaultFormProps } from '../fixtures';
5 |
6 | test('SyncTokenForm renders with correct snapshot', () => {
7 | const component = TestRenderer.create();
8 |
9 | expect(component).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/tests/components/WelcomePanel.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as TestRenderer from 'react-test-renderer';
3 | import WelcomePanel from '../../src/ts/components/WelcomePanel';
4 |
5 | test('WelcomePanel renders basic layout', () => {
6 | const component = TestRenderer.create();
7 |
8 | expect(component).toMatchSnapshot();
9 | });
10 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/AdvancedConfigForm.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`AdvancedConfigForm component Renders basic form 1`] = `
4 |
52 | `;
53 |
54 | exports[`AdvancedConfigForm component Renders form with values 1`] = `
55 |
103 | `;
104 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/ExportComments.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ExportComments component Disables submit button during export 1`] = `
4 |
25 | `;
26 |
27 | exports[`ExportComments component Renders basic form without any export logs 1`] = `
28 |
49 | `;
50 |
51 | exports[`ExportComments component Renders export logs 1`] = `
52 |
132 | `;
133 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/HelpResources.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SupportLinks renders with correct links 1`] = `
4 |
37 | `;
38 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/Message.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Message Includes message content and class name 1`] = `
4 |
7 |
8 | Test message
9 |
10 |
11 | `;
12 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/SSOConfigForm.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SSOConfigForm component Renders basic form 1`] = `
4 |
85 | `;
86 |
87 | exports[`SSOConfigForm component Renders form with values 1`] = `
88 |
169 | `;
170 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/SupportLinks.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SupportLinks renders with correct links 1`] = `
4 |
7 | -
10 |
21 |
22 | -
25 |
36 |
37 | -
40 |
51 |
52 |
53 | `;
54 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/SyncConfigForm.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SyncConfigForm component Renders form with disabled sync subscription 1`] = `
4 |
45 | `;
46 |
47 | exports[`SyncConfigForm component Renders form with enabled sync subscription 1`] = `
48 |
89 | `;
90 |
91 | exports[`SyncConfigForm component Renders form with no sync enabled 1`] = `
92 |
133 | `;
134 |
135 | exports[`SyncConfigForm component Renders form with paused sync subscription 1`] = `
136 |
177 | `;
178 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/SyncTokenForm.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`SyncTokenForm renders with correct snapshot 1`] = `
4 |
61 | `;
62 |
--------------------------------------------------------------------------------
/frontend/tests/components/__snapshots__/WelcomePanel.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`WelcomePanel renders basic layout 1`] = `
4 |
7 |
10 |
13 | Manage Your Community
14 |
15 |
18 |
21 |
22 | Comments
23 |
24 |
61 |
62 |
65 |
66 | Polls
67 |
68 |
88 |
89 |
92 |
93 | Analytics
94 |
95 |
124 |
125 |
128 |
129 | Settings
130 |
131 |
169 |
170 |
171 |
172 |
173 | `;
174 |
--------------------------------------------------------------------------------
/frontend/tests/containers/mapStateToProps.test.ts:
--------------------------------------------------------------------------------
1 | import { IFormProps } from '../../src/ts/components/FormProps';
2 | import mapStateToProps from '../../src/ts/containers/mapStateToProps';
3 | import AdminState from '../../src/ts/reducers/AdminState';
4 |
5 | declare var global: any
6 |
7 | describe('containers/mapStateToProps', () => {
8 |
9 | test('returns the expected props shape', () => {
10 | const adminState: AdminState = new AdminState(global.DISQUS_WP);
11 | const props: any = mapStateToProps(adminState);
12 |
13 | expect(props.data).toEqual(adminState);
14 | });
15 |
16 | });
17 |
--------------------------------------------------------------------------------
/frontend/tests/fixtures.ts:
--------------------------------------------------------------------------------
1 | import { IFormProps } from '../src/ts/components/FormProps';
2 | import AdminState, { IAdminConfigData } from '../src/ts/reducers/AdminState';
3 |
4 | export const getDefaultAdminConfig = (): IAdminConfigData => ({
5 | adminUrls: {
6 | disqus: 'https://foo.com/wp-admin/admin.php?page=disqus',
7 | editComments: 'https://foo.com/wp-admin/edit-comments.php',
8 | },
9 | permissions: {
10 | canManageSettings: true,
11 | },
12 | rest: {
13 | base: 'https://foo.com/wp-json/',
14 | disqusBase: 'disqus/v1/',
15 | nonce: 'foo',
16 | },
17 | site: {
18 | allPlugins: {},
19 | name: 'Test',
20 | phpVersion: '5.4',
21 | pluginVersion: '0.0.1',
22 | wordpressVersion: '4.9.4',
23 | },
24 | });
25 |
26 | export const getDefaultFormProps = (): IFormProps => ({
27 | data: new AdminState(getDefaultAdminConfig()),
28 | onCopyText: jest.fn(),
29 | onDateSelectorInputchange: jest.fn(),
30 | onGenerateRandomSyncToken: jest.fn(),
31 | onInputChange: jest.fn(),
32 | onSubmitExportCommentsForm: jest.fn(),
33 | onSubmitManualSyncForm: jest.fn(),
34 | onSubmitSiteForm: jest.fn(),
35 | onSubmitSyncConfigForm: jest.fn(),
36 | onToggleState: jest.fn(),
37 | onUpdateInstallationState: jest.fn(),
38 | });
39 |
--------------------------------------------------------------------------------
/frontend/tests/reducers/AdminOptions.test.ts:
--------------------------------------------------------------------------------
1 | import AdminOptions from '../../src/ts/reducers/AdminOptions';
2 |
3 | describe('AdminOptions', () => {
4 |
5 | test('Constructor populates values', () => {
6 | const adminOptions: AdminOptions = new AdminOptions({
7 | disqus_forum_url: 'foo',
8 | });
9 |
10 | expect(adminOptions.disqus_forum_url).toBe('foo');
11 | expect(adminOptions.disqus_public_key).toBeNull();
12 | });
13 |
14 | test('Immutable.Record functions are implemented', () => {
15 | const adminOptions: AdminOptions = new AdminOptions();
16 |
17 | expect(adminOptions.set('disqus_forum_url', 'foo').disqus_forum_url).toBe('foo');
18 |
19 | expect(adminOptions.with({
20 | disqus_forum_url: 'bar',
21 | }).disqus_forum_url).toBe('bar');
22 | });
23 |
24 | });
25 |
--------------------------------------------------------------------------------
/frontend/tests/reducers/AdminState.test.ts:
--------------------------------------------------------------------------------
1 | import AdminState from '../../src/ts/reducers/AdminState';
2 |
3 | declare var global: any
4 |
5 | describe('AdminState', () => {
6 |
7 | test('Constructor populates default values', () => {
8 | const adminState: AdminState = new AdminState(global.DISQUS_WP);
9 |
10 | expect(adminState.config).toEqual(global.DISQUS_WP);
11 | expect(adminState.exportLogs.count()).toBe(0);
12 | });
13 |
14 | test('Immutable.Record functions are implemented', () => {
15 | const adminState: AdminState = new AdminState(global.DISQUS_WP);
16 |
17 | expect(adminState.set('activeTab', 'foo').activeTab).toBe('foo');
18 |
19 | expect(adminState.with({
20 | activeTab: 'bar',
21 | }).activeTab).toBe('bar');
22 | });
23 |
24 | });
25 |
--------------------------------------------------------------------------------
/frontend/tests/reducers/SyncStatus.test.ts:
--------------------------------------------------------------------------------
1 | import SyncStatus from '../../src/ts/reducers/SyncStatus';
2 |
3 | describe('SyncStatus', () => {
4 |
5 | test('Constructor populates values', () => {
6 | const syncStatus: SyncStatus = new SyncStatus({
7 | enabled: true,
8 | });
9 |
10 | expect(syncStatus.enabled).toBe(true);
11 | expect(syncStatus.last_message).toBeNull();
12 | });
13 |
14 | test('Immutable.Record functions are implemented', () => {
15 | const syncStatus: SyncStatus = new SyncStatus();
16 |
17 | expect(syncStatus.set('last_message', 'foo').last_message).toBe('foo');
18 |
19 | expect(syncStatus.with({
20 | last_message: 'bar',
21 | }).last_message).toBe('bar');
22 | });
23 |
24 | });
25 |
--------------------------------------------------------------------------------
/frontend/tests/reducers/adminApp.test.ts:
--------------------------------------------------------------------------------
1 |
2 | import * as actions from '../../src/ts/actions';
3 | import { IAction } from '../../src/ts/actions';
4 | import adminApp from '../../src/ts/reducers/adminApp';
5 | import AdminState, {
6 | ExportLogStaus,
7 | IExportPostLog,
8 | IMessage,
9 | InstallationState,
10 | } from '../../src/ts/reducers/AdminState';
11 | import SyncStatus from '../../src/ts/reducers/SyncStatus';
12 |
13 | describe('adminApp reducer', () => {
14 |
15 | test('changeInstallStateAction updates the installationState', () => {
16 | const action: IAction = actions.changeInstallStateAction(InstallationState.installed);
17 |
18 | const newState: AdminState = adminApp(undefined, action);
19 |
20 | expect(newState.installationState).toBe(InstallationState.installed);
21 | });
22 |
23 | test('changeTabStateAction updates the activeTab', () => {
24 | const action: IAction = actions.changeTabStateAction('foo');
25 |
26 | const newState: AdminState = adminApp(undefined, action);
27 |
28 | expect(newState.activeTab).toBe('foo');
29 | });
30 |
31 | test('setMessageAction updates the message', () => {
32 | const message: IMessage = {
33 | text: 'foo',
34 | type: 'bar',
35 | };
36 | const action: IAction = actions.setMessageAction(message);
37 |
38 | const newState: AdminState = adminApp(undefined, action);
39 |
40 | expect(newState.message).toBe(message);
41 | });
42 |
43 | test('setValueAction updates any value', () => {
44 | const action: IAction = actions.setValueAction('isFetchingAdminOptions', true);
45 |
46 | const newState: AdminState = adminApp(undefined, action);
47 |
48 | expect(newState.isFetchingAdminOptions).toBe(true);
49 | });
50 |
51 | test('toggleValueAction changes a boolean value', () => {
52 | const action1: IAction = actions.toggleValueAction('isSiteFormLocked');
53 |
54 | const newState1: AdminState = adminApp(undefined, action1);
55 |
56 | expect(newState1.isSiteFormLocked).toBe(false);
57 |
58 | const action2: IAction = actions.toggleValueAction('isSiteFormLocked');
59 |
60 | const newState2: AdminState = adminApp(newState1, action2);
61 |
62 | expect(newState2.isSiteFormLocked).toBe(true);
63 | });
64 |
65 | test('updateAdminOptionsAction updates adminOptions', () => {
66 | const action: IAction = actions.updateAdminOptionsAction({
67 | disqus_forum_url: 'foo',
68 | });
69 |
70 | const newState: AdminState = adminApp(undefined, action);
71 |
72 | expect(newState.adminOptions.disqus_forum_url).toBe('foo');
73 | });
74 |
75 | test('updateExportPostLogAction updates exportLogs', () => {
76 | const postLog1: IExportPostLog = {
77 | id: 1,
78 | link: 'https://foo.com?p=1',
79 | status: ExportLogStaus.pending,
80 | title: 'Test Title',
81 | };
82 |
83 | const action1: IAction = actions.updateExportPostLogAction(postLog1);
84 |
85 | const newState1: AdminState = adminApp(undefined, action1);
86 |
87 | expect(newState1.exportLogs.get(1)).toBe(postLog1);
88 |
89 | const postLog2: IExportPostLog = {
90 | id: 1,
91 | link: 'https://foo.com?p=1',
92 | numComments: 10,
93 | status: ExportLogStaus.complete,
94 | title: 'Test Title',
95 | };
96 |
97 | const action2: IAction = actions.updateExportPostLogAction(postLog2);
98 |
99 | const newState2: AdminState = adminApp(newState1, action2);
100 |
101 | expect(newState2.exportLogs.get(1)).toBe(postLog2);
102 | });
103 |
104 | test('updateLocalOptionAction updates localAdminOptions', () => {
105 | const action: IAction = actions.updateLocalOptionAction('disqus_forum_url', 'foo');
106 |
107 | const newState: AdminState = adminApp(undefined, action);
108 |
109 | expect(newState.localAdminOptions.disqus_forum_url).toBe('foo');
110 | });
111 |
112 | test('updateSyncStatusAction updates syncStatus', () => {
113 | const syncStatus: SyncStatus = new SyncStatus({
114 | enabled: true,
115 | last_message: 'foo',
116 | requires_update: false,
117 | subscribed: true,
118 | subscription: 1,
119 | });
120 |
121 | const action: IAction = actions.updateSyncStatusAction(syncStatus);
122 |
123 | const newState: AdminState = adminApp(undefined, action);
124 |
125 | expect(newState.syncStatus).toEqual(syncStatus);
126 | });
127 |
128 | });
129 |
--------------------------------------------------------------------------------
/frontend/tests/typings/NodeJS.d.ts:
--------------------------------------------------------------------------------
1 | declare module NodeJS {
2 | interface Global {
3 | __: (key: string) => string,
4 | DISQUS_WP: any,
5 | document: any,
6 | XMLHttpRequest: any,
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/frontend/tests/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { createRandomToken, getForumAdminUrl } from '../src/ts/utils';
2 |
3 | test('getForumAdminUrl returns fully qualified URL', () => {
4 | expect(getForumAdminUrl('foo', 'settings')).toBe('https://foo.disqus.com/admin/settings/');
5 | });
6 |
7 | test('createRandomToken returns a token containing specified number of characters', () => {
8 | const token = createRandomToken(32);
9 |
10 | expect(token.length).toBe(32);
11 | expect(token.match(/[a-z0-9]/));
12 | });
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "disqus-wordpress-plugin",
3 | "version": "3.1.2",
4 | "description": "A WordPress plugin that allows site owners to easily replace the default commenting system with Disqus",
5 | "main": "frontend/src/js/index.js",
6 | "scripts": {
7 | "build": "yarn run build:dev && yarn run build:dist",
8 | "build:dev": "webpack --mode development",
9 | "build:dist": "NODE_ENV=production webpack --mode production",
10 | "start": "webpack --mode development --watch",
11 | "test": "jest"
12 | },
13 | "author": "ryanvalentin ",
14 | "license": "ISC",
15 | "devDependencies": {
16 | "@types/jest": "^22.2.0",
17 | "@types/react": "^16.0.40",
18 | "@types/react-dom": "^16.0.4",
19 | "@types/react-redux": "^5.0.15",
20 | "awesome-typescript-loader": "5.0.0-0",
21 | "babel-core": "^6.26.3",
22 | "babel-jest": "^22.4.1",
23 | "babel-preset-env": "^1.6.1",
24 | "babel-preset-react": "^6.24.1",
25 | "i18n-webpack-plugin": "^1.0.0",
26 | "jest": "^24.0.0",
27 | "react-test-renderer": "^16.2.0",
28 | "source-map-loader": "^0.2.3",
29 | "ts-jest": "^24.0.0",
30 | "tslint": "^5.9.1",
31 | "tslint-react": "^3.5.1",
32 | "typescript": "^2.7.2",
33 | "webpack": "^4.20.2",
34 | "webpack-cli": "^3.1.1"
35 | },
36 | "dependencies": {
37 | "@types/react-test-renderer": "^16.8.1",
38 | "immutable": "^3.8.2",
39 | "moment": "^2.21.0",
40 | "react": "^16.2.0",
41 | "react-dom": "^16.2.0",
42 | "react-redux": "^5.0.7",
43 | "redux": "^3.7.2"
44 | },
45 | "repository": "https://github.com/ryanvalentin/disqus-wordpress-plugin.git",
46 | "jest": {
47 | "transform": {
48 | "^.+\\.tsx?$": "ts-jest"
49 | },
50 | "testRegex": "(/frontend/tests/.*(\\.|/)(test|spec))\\.(ts|tsx)$",
51 | "moduleFileExtensions": [
52 | "js",
53 | "jsx",
54 | "ts",
55 | "tsx",
56 | "json"
57 | ],
58 | "setupFiles": [
59 | "./frontend/tests/beforeAll.ts"
60 | ]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/phpcs.ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Same as for WordPress plugins with some small changes
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 3
26 |
27 |
28 | 3
29 |
30 |
31 | ./disqus
32 |
33 | *.js
34 | */index.php
35 |
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | array( PLUGIN_PATH ),
25 | );
26 |
27 | // Give access to tests_add_filter() function.
28 | require_once $_tests_dir . '/includes/functions.php';
29 |
30 | /**
31 | * Manually load the plugin being tested.
32 | */
33 | function _manually_load_plugin() {
34 | require dirname( __DIR__ ) . '/' . PLUGIN_FILE;
35 | }
36 | tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
37 |
38 | // Start up the WP testing environment.
39 | require $_tests_dir . '/includes/bootstrap.php';
40 |
--------------------------------------------------------------------------------
/tests/test-admin.php:
--------------------------------------------------------------------------------
1 | assertEquals( 32, strlen( get_option( 'disqus_sync_token' ) ) );
17 | }
18 |
19 | /**
20 | * Check that the REST URL filter doesn't replace the host when they're the same.
21 | */
22 | function test_dsq_filter_rest_url_same_host() {
23 | $admin = new Disqus_Admin( 'disqus', '0.0.0', 'foo' );
24 |
25 | $rest_url = $admin->dsq_filter_rest_url( 'https://example.org/wp-json/disqus/v1' );
26 |
27 | $this->assertEquals( 'https://example.org/wp-json/disqus/v1', $rest_url );
28 | }
29 |
30 | /**
31 | * Check that the REST URL filter does replace the host when they're different.
32 | */
33 | function test_dsq_filter_rest_url_different_host() {
34 | $admin = new Disqus_Admin( 'disqus', '0.0.0', 'foo' );
35 | $previous_host = $_SERVER['HTTP_HOST'];
36 | $_SERVER['HTTP_HOST'] = 'bar.com';
37 |
38 | $rest_url = $admin->dsq_filter_rest_url( 'https://example.org/wp-json/disqus/v1' );
39 |
40 | $this->assertEquals( 'https://bar.com/wp-json/disqus/v1', $rest_url );
41 |
42 | $_SERVER['HTTP_HOST'] = $previous_host;
43 | }
44 |
45 | /**
46 | * Ensure that REST URL filter will not error when HTTP_HOST is undefined.
47 | */
48 | function test_dsq_filter_rest_url_no_host() {
49 | $admin = new Disqus_Admin( 'disqus', '0.0.0', 'foo' );
50 | $previous_host = $_SERVER['HTTP_HOST'];
51 | $_SERVER['HTTP_HOST'] = NULL;
52 | $init_url = 'https://example.org/wp-json/disqus/v1';
53 |
54 | $rest_url = $admin->dsq_filter_rest_url($init_url);
55 |
56 | $this->assertEquals( $init_url, $rest_url );
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/tests/test-api-service.php:
--------------------------------------------------------------------------------
1 | json_encode( array(
13 | 'code' => 0,
14 | 'response' => array(
15 | 'url' => $url,
16 | 'args' => $args,
17 | ),
18 | ) ),
19 | );
20 | }
21 |
22 | function filter_http_force_error( $preempt, $args, $url ) {
23 | return new WP_Error( 'error', 'There was a generic error.' );
24 | }
25 |
26 | class Test_Api_Service extends WP_UnitTestCase {
27 |
28 | public function set_up() {
29 | parent::set_up();
30 |
31 | // Filter HTTP requests made from `wp_remote_get` and `wp_remote_post` so we don't actually call server.
32 | add_filter( 'pre_http_request', 'reflect_params', 1, 3 );
33 | }
34 |
35 | public function tear_down() {
36 | parent::tear_down();
37 |
38 | remove_filter( 'pre_http_request', 'reflect_params', 1 );
39 | }
40 |
41 | /**
42 | * Check that Disqus API GET requests include the secret key and access token without passing it explicitly.
43 | */
44 | public function test_api_get_tokens() {
45 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
46 |
47 | $api_data = $api_service->api_get( 'forums/details', array() );
48 |
49 | $this->assertNotFalse( strpos( $api_data->response->url, 'forums/details.json' ) );
50 | $this->assertNotFalse( strpos( $api_data->response->url, 'api_secret=APISECRETKEY' ) );
51 | $this->assertNotFalse( strpos( $api_data->response->url, 'access_token=ACCESSTOKEN' ) );
52 | }
53 |
54 | /**
55 | * Check that Disqus API POST requests include the secret key and access token without passing it explicitly.
56 | */
57 | public function test_api_post_tokens() {
58 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
59 |
60 | $api_data = $api_service->api_post( 'forums/update', array() );
61 |
62 | $this->assertNotFalse( strpos( $api_data->response->url, 'forums/update.json' ) );
63 | $this->assertNotFalse( strpos( $api_data->response->url, 'api_secret=APISECRETKEY' ) );
64 | $this->assertNotFalse( strpos( $api_data->response->url, 'access_token=ACCESSTOKEN' ) );
65 | }
66 |
67 | /**
68 | * Check that Disqus API GET requests include params that are passed in
69 | */
70 | public function test_api_get_with_args() {
71 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
72 |
73 | $api_data = $api_service->api_get( 'forums/details', array(
74 | 'forum' => 'bobross',
75 | 'attach' => array( 'forumDaysAlive', 'forumFeatures' ),
76 | ) );
77 |
78 | $this->assertNotFalse( strpos( $api_data->response->url, 'forum=bobross' ) );
79 | $this->assertNotFalse( strpos( $api_data->response->url, 'attach=forumDaysAlive' ) );
80 | $this->assertNotFalse( strpos( $api_data->response->url, 'attach=forumFeatures' ) );
81 | }
82 |
83 | /**
84 | * Check that Disqus API GET requests include params that are passed in
85 | */
86 | public function test_api_post_with_args() {
87 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
88 |
89 | $api_data = $api_service->api_post( 'forums/update', array(
90 | 'forum' => 'bobross',
91 | 'name' => 'Ross Bob',
92 | 'attach' => array( 'forumDaysAlive', 'forumFeatures' ),
93 | ) );
94 |
95 | $this->assertObjectHasAttribute( 'name', $api_data->response->args->body );
96 | $this->assertEquals( 'Ross Bob', $api_data->response->args->body->name );
97 |
98 | $this->assertObjectHasAttribute( 'attach', $api_data->response->args->body );
99 | $this->assertTrue( is_array( $api_data->response->args->body->attach ) );
100 | }
101 |
102 | /**
103 | * Check that Disqus API POST requests have no referer.
104 | */
105 | public function test_api_post_no_referer() {
106 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
107 |
108 | $api_data = $api_service->api_post( 'forums/details', array() );
109 |
110 | $this->assertEquals( '', $api_data->response->args->headers->Referer );
111 | }
112 |
113 | /**
114 | * Check that the Disqus API returns a proper error.
115 | */
116 | public function test_api_error() {
117 | remove_filter( 'pre_http_request', 'reflect_params', 1 );
118 | add_filter( 'pre_http_request', 'filter_http_force_error', 1, 3 );
119 |
120 | $api_service = new Disqus_Api_Service( 'APISECRETKEY', 'ACCESSTOKEN' );
121 |
122 | $api_data = $api_service->api_get( 'forums/details', array(
123 | 'forum' => 'bobross',
124 | 'attach' => array( 'forumDaysAlive', 'forumFeatures' ),
125 | ) );
126 |
127 | $this->assertGreaterThan( 0, $api_data->code );
128 | $this->assertEquals( 'There was a generic error.', $api_data->response );
129 |
130 | remove_filter( 'pre_http_request', 'filter_http_force_error', 1 );
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/tests/test-plugin.php:
--------------------------------------------------------------------------------
1 | assertTrue( is_plugin_active( PLUGIN_PATH ) );
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/tests/test-rest-api-basic.php:
--------------------------------------------------------------------------------
1 | admin_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
33 | $this->subscriber_user_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
34 |
35 | global $wp_rest_server;
36 |
37 | $this->server = $wp_rest_server = new \WP_REST_Server;
38 |
39 | do_action( 'rest_api_init' );
40 | }
41 |
42 | /**
43 | * Check that the Disqus custom REST routes have been registered.
44 | */
45 | public function test_custom_routes_registered() {
46 | $routes = $this->server->get_routes();
47 | $custom_routes = array(
48 | DISQUS_REST_NAMESPACE,
49 | DISQUS_REST_NAMESPACE . '/sync/webhook',
50 | DISQUS_REST_NAMESPACE . '/sync/status',
51 | DISQUS_REST_NAMESPACE . '/sync/enable',
52 | DISQUS_REST_NAMESPACE . '/sync/disable',
53 | DISQUS_REST_NAMESPACE . '/settings'
54 | );
55 |
56 | foreach ( $custom_routes as $custom_route ) {
57 | $this->assertArrayHasKey( $custom_route, $routes, 'Custom route "' . $custom_route . '" not registered' );
58 | }
59 | }
60 |
61 | /**
62 | * Check that the Disqus REST routes can be called.
63 | */
64 | public function test_custom_routes_callable() {
65 | $disqus_route = DISQUS_REST_NAMESPACE;
66 | $routes = $this->server->get_routes();
67 | foreach( $routes as $route => $route_config ) {
68 | if ( 0 === strpos( $disqus_route, $route ) ) {
69 | $this->assertTrue( is_array( $route_config ) );
70 | foreach ( $route_config as $i => $endpoint ) {
71 | $this->assertArrayHasKey( 'callback', $endpoint );
72 | $this->assertArrayHasKey( 0, $endpoint[ 'callback' ], get_class( $this ) );
73 | $this->assertArrayHasKey( 1, $endpoint[ 'callback' ], get_class( $this ) );
74 | $this->assertTrue( is_callable( array( $endpoint['callback'][0], $endpoint['callback'][1] ) ) );
75 | }
76 | }
77 | }
78 | }
79 |
80 | /**
81 | * Check that we can fetch the Disqus plugin settings as a WordPress admin.
82 | */
83 | public function test_admin_fetch_settings() {
84 | wp_set_current_user( $this->admin_user_id );
85 |
86 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
87 | $response = $this->server->dispatch( $request );
88 |
89 | $this->assertEquals( 200, $response->get_status() );
90 | }
91 |
92 | /**
93 | * Check that we can't fetch or update settings without authentication.
94 | */
95 | public function test_no_authentication_settings() {
96 | wp_set_current_user( null );
97 |
98 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
99 | $response = $this->server->dispatch( $request );
100 | $this->assertEquals( 401, $response->get_status() );
101 | }
102 |
103 | /**
104 | * Check that we can't fetch or update settings without admin authentication.
105 | */
106 | public function test_lower_permission_settings() {
107 | wp_set_current_user( $this->subscriber_user_id );
108 |
109 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
110 | $response = $this->server->dispatch( $request );
111 | $this->assertEquals( 401, $response->get_status() );
112 | }
113 |
114 | /**
115 | * Check that we can fetch settings with shared secret authentication.
116 | */
117 | public function test_valid_shared_secret() {
118 | wp_set_current_user( null );
119 | update_option( 'disqus_sync_token', 'valid_token' );
120 |
121 | $body = json_encode( array(
122 | 'disqus_forum_url' => 'rossbob',
123 | ) );
124 | $hub_signature = hash_hmac( 'sha512', $body, 'valid_token' );
125 |
126 | $request = new WP_REST_Request( 'POST', DISQUS_REST_NAMESPACE . '/settings' );
127 | $request->set_body( $body );
128 | $request->set_header( 'Content-Type', 'application/json' );
129 | $request->set_header( 'X-Hub-Signature', 'sha512=' . $hub_signature );
130 |
131 | $response = $this->server->dispatch( $request );
132 | $this->assertEquals( 200, $response->get_status() );
133 | }
134 |
135 | /**
136 | * Check that we can't fetch settings with bad secret authentication.
137 | */
138 | public function test_invalid_shared_secret() {
139 | wp_set_current_user( null );
140 | update_option( 'disqus_sync_token', 'valid_token' );
141 |
142 | $body = json_encode( array(
143 | 'disqus_forum_url' => 'rossbob',
144 | ) );
145 | $hub_signature = hash_hmac( 'sha512', $body, 'not_valid_token' );
146 |
147 | $request = new WP_REST_Request( 'POST', DISQUS_REST_NAMESPACE . '/settings' );
148 | $request->set_body_params( $body );
149 | $request->set_header( 'X-Hub-Signature', 'sha512=' . $hub_signature );
150 |
151 | $response = $this->server->dispatch( $request );
152 | $this->assertEquals( 401, $response->get_status() );
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/tests/test-rest-api-export.php:
--------------------------------------------------------------------------------
1 | getMethod('is_comment_exportable');
46 | $method->setAccessible(true);
47 | return $method->invokeArgs( $this->disqus_rest_api, array( $comment ));
48 | }
49 |
50 | public function set_up() {
51 | parent::set_up();
52 |
53 | $this->post = get_post( $this->factory->post->create() );
54 |
55 | $this->comment = get_comment(
56 | $this->factory->comment->create(
57 | array (
58 | 'comment_post_ID' => $this->post->ID
59 | )
60 | )
61 | );
62 |
63 | $this->disqus_rest_api = new Disqus_Rest_Api(null, null);
64 | }
65 |
66 | /**
67 | * Test that the default comment is exportable.
68 | */
69 | public function test_default_comment_is_exportable() {
70 | $this->assertTrue( $this->is_comment_exportable( $this->comment ) );
71 | }
72 |
73 | /**
74 | * Test that a comment synced from Disqus is not exportable.
75 | */
76 | public function test_disqus_comment_is_not_exportable() {
77 | $disqus_comment = get_comment(
78 | $this->factory->comment->create(
79 | array (
80 | 'comment_post_ID' => $this->post->ID,
81 | 'comment_agent' => 'Disqus Sync Host'
82 | )
83 | )
84 | );
85 |
86 | $this->assertFalse( $this->is_comment_exportable( $disqus_comment ) );
87 | }
88 | }
--------------------------------------------------------------------------------
/tests/test-rest-api-settings.php:
--------------------------------------------------------------------------------
1 | admin_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
26 |
27 | global $wp_rest_server;
28 |
29 | $this->server = $wp_rest_server = new \WP_REST_Server;
30 |
31 | do_action( 'rest_api_init' );
32 | }
33 |
34 | /**
35 | * Check that we can fetch the Disqus plugin settings as a WordPress admin.
36 | */
37 | public function test_admin_fetch_settings() {
38 | wp_set_current_user( $this->admin_user_id );
39 |
40 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
41 | $response = $this->server->dispatch( $request );
42 | $response_data = $response->get_data();
43 |
44 | $this->assertEquals( 200, $response->get_status() );
45 | $this->assertArrayHasKey( 'data', $response_data );
46 | $this->assertTrue( is_array( $response_data['data'] ) );
47 | $this->assertArrayHasKey( 'disqus_forum_url', $response_data['data'] );
48 | $this->assertArrayHasKey( 'disqus_sso_enabled', $response_data['data'] );
49 | $this->assertArrayHasKey( 'disqus_public_key', $response_data['data'] );
50 | $this->assertArrayHasKey( 'disqus_secret_key', $response_data['data'] );
51 | $this->assertArrayHasKey( 'disqus_admin_access_token', $response_data['data'] );
52 | $this->assertArrayHasKey( 'disqus_sso_button', $response_data['data'] );
53 | $this->assertArrayHasKey( 'disqus_sync_token', $response_data['data'] );
54 | $this->assertArrayHasKey( 'disqus_installed', $response_data['data'] );
55 | }
56 |
57 | /**
58 | * Check that the REST API reports the plugin as uninstalled without correct options.
59 | */
60 | public function test_admin_fetch_settings_uninstalled() {
61 | wp_set_current_user( $this->admin_user_id );
62 | update_option( 'disqus_forum_url', '' );
63 |
64 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
65 | $response = $this->server->dispatch( $request );
66 | $response_data = $response->get_data();
67 |
68 | $this->assertEquals( '', $response_data['data']['disqus_forum_url'] );
69 | $this->assertFalse( $response_data['data']['disqus_installed'] );
70 | }
71 |
72 | /**
73 | * Check that the REST API reports the plugin as installed with correct options.
74 | */
75 | public function test_admin_fetch_settings_installed() {
76 | wp_set_current_user( $this->admin_user_id );
77 | update_option( 'disqus_forum_url', 'bobross' );
78 |
79 | $request = new WP_REST_Request( 'GET', DISQUS_REST_NAMESPACE . '/settings' );
80 | $response = $this->server->dispatch( $request );
81 | $response_data = $response->get_data();
82 |
83 | $this->assertEquals( 'bobross', $response_data['data']['disqus_forum_url'] );
84 | $this->assertTrue( $response_data['data']['disqus_installed'] );
85 | }
86 |
87 | /**
88 | * Check that we can update the Disqus plugin settings as a WordPress admin.
89 | */
90 | public function test_admin_update_settings() {
91 | wp_set_current_user( $this->admin_user_id );
92 |
93 | $body = array(
94 | 'disqus_forum_url' => 'rossbob',
95 | );
96 |
97 | $request = new WP_REST_Request( 'POST', DISQUS_REST_NAMESPACE . '/settings' );
98 | $request->set_body_params( $body );
99 |
100 | $response = $this->server->dispatch( $request );
101 | $response_data = $response->get_data();
102 |
103 | $this->assertEquals( 200, $response->get_status() );
104 | $this->assertEquals( 'rossbob', $response_data['data']['disqus_forum_url'] );
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./disqus/admin/js/",
4 | "sourceMap": true,
5 | "skipLibCheck": true,
6 | "noImplicitAny": true,
7 | "module": "commonjs",
8 | "target": "es2015",
9 | "jsx": "react"
10 | },
11 | "include": [
12 | "./frontend/src/ts/**/*"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "defaultSeverity": "warn",
3 | "extends": [
4 | "tslint:recommended",
5 | "tslint-react"
6 | ],
7 | "jsRules": {},
8 | "rules": {
9 | "curly": false,
10 | "quotemark": [true, "single"],
11 | "max-line-length": [true, 120],
12 | "jsx-no-multiline-js": false
13 | },
14 | "rulesDirectory": []
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var package = require('./package.json');
2 | var path = require('path');
3 | var webpack = require('webpack');
4 | var I18nPlugin = require('i18n-webpack-plugin');
5 |
6 | var OPTIMIZE = process.env.NODE_ENV === 'production';
7 |
8 | var LANGUAGES = {
9 | 'en': null,
10 | };
11 |
12 | var allConfigs = Object.keys(LANGUAGES).map(function (language) {
13 | var config = {
14 | entry: './frontend/src/ts/index.ts',
15 | output: {
16 | path: path.resolve(__dirname, 'disqus/admin/bundles/js'),
17 | filename: language + '.disqus-admin.bundle.' + package.version + (OPTIMIZE ? '.min.js' : '.js'),
18 | },
19 | devtool: 'source-map',
20 | resolve: {
21 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
22 | },
23 | module: {
24 | rules: [{
25 | test: /\.tsx?$/,
26 | loader: 'awesome-typescript-loader',
27 | }, {
28 | enforce: 'pre',
29 | test: /\.js$/,
30 | loader: 'source-map-loader',
31 | }],
32 | },
33 | plugins: [
34 | new I18nPlugin(LANGUAGES[language]),
35 | ],
36 | };
37 |
38 | if (OPTIMIZE) {
39 | // Tells React to use production build: https://facebook.github.io/react/docs/optimizing-performance.html#use-the-production-build
40 | config.plugins.push(new webpack.DefinePlugin({
41 | 'process.env': {
42 | NODE_ENV: JSON.stringify('production'),
43 | },
44 | }));
45 | }
46 |
47 | return config;
48 | });
49 |
50 | module.exports = allConfigs;
51 |
--------------------------------------------------------------------------------
/wp-su.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This is a wrapper so that wp-cli can run as the www-data user so that permissions
3 | # remain correct
4 | sudo -E -u www-data /bin/wp-cli.phar $*
5 |
--------------------------------------------------------------------------------