Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ workflows:
only:
- develop
- PM-3686_group-submissions-in-challenge-details
- payload-cms

- "build-prod":
context: org-global
Expand Down
31 changes: 31 additions & 0 deletions __tests__/server/contentful-endpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
getContentfulApiBaseUrl,
getContentfulApiHost,
} from 'server/services/contentful-endpoints';

describe('server/services/contentful-endpoints', () => {
test('retains Contentful Delivery and Preview hosts by default', () => {
expect(getContentfulApiHost({}, false)).toBe('cdn.contentful.com');
expect(getContentfulApiHost({}, true)).toBe('preview.contentful.com');
});

test('uses configured compatibility hosts and normalizes URL syntax', () => {
const environment = {
CDN_API_HOST: 'https://cms.topcoder-dev.com/',
PREVIEW_API_HOST: 'cms.topcoder-dev.com',
};

expect(getContentfulApiHost(environment, false)).toBe('cms.topcoder-dev.com');
expect(getContentfulApiHost(environment, true)).toBe('cms.topcoder-dev.com');
});

test('builds the Contentful-compatible spaces and environments path', () => {
expect(getContentfulApiBaseUrl('cms.topcoder-dev.com', 'space id', 'feature/test'))
.toBe('https://cms.topcoder-dev.com/spaces/space%20id/environments/feature%2Ftest');
});

test('rejects non-string configured hosts', () => {
expect(() => getContentfulApiHost({ CDN_API_HOST: true }, false))
.toThrow('CDN_API_HOST must be a hostname string.');
});
});
51 changes: 51 additions & 0 deletions __tests__/server/contentful.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/* eslint-env jest */

import { createClient as createDeliveryClient } from 'contentful';
import {
articleVote,
getService,
} from 'server/services/contentful';

const contentfulManagement = require('contentful-management');

jest.mock('contentful', () => ({
createClient: jest.fn(() => ({})),
}));

jest.mock('contentful-management', () => ({
createClient: jest.fn(() => ({
getSpace: jest.fn(() => Promise.resolve({
getEnvironment: jest.fn(() => Promise.resolve({
getEntry: jest.fn(() => Promise.resolve({
fields: {},
update: jest.fn(() => Promise.resolve({
publish: jest.fn(() => Promise.resolve({ published: true })),
})),
})),
})),
})),
})),
}));

describe('server/services/contentful HTTPS connections', () => {
test('shares one keep-alive agent across Delivery, Preview, and Management clients', async () => {
getService('default', 'master', false);

const deliveryAgents = createDeliveryClient.mock.calls
.map(call => call[0].httpsAgent);

expect(deliveryAgents.length).toBeGreaterThan(1);
deliveryAgents.forEach((agent) => {
expect(agent).toBe(deliveryAgents[0]);
expect(agent.options.keepAlive).toBe(true);
});

await articleVote({
id: 'article-id',
votes: { downvotes: 1, upvotes: 2 },
});

const managementConfig = contentfulManagement.createClient.mock.calls[0][0];
expect(managementConfig.httpsAgent).toBe(deliveryAgents[0]);
});
});
86 changes: 86 additions & 0 deletions __tests__/shared/containers/EDUTrackCards.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import Renderer from 'react-test-renderer/shallow';
import TrackCards from 'containers/EDU/partials/TrackCards';

const EXPECTED_SELECT = [
'sys.id',
'sys.type',
'fields.externalArticle',
'fields.contentUrl',
'fields.slug',
'fields.title',
'fields.tags',
'fields.readTime',
'fields.creationDate',
'fields.upvotes',
'fields.commentsCount',
'fields.featuredImage',
'fields.contentAuthor',
'fields.file',
].join(',');

test('selects only fields required by EDU track cards and their assets', () => {
const renderer = new Renderer();
renderer.render(<TrackCards track="Development" theme={{ trackCards: 'cards' }} />);

expect(renderer.getRenderOutput().props.entryQueries).toEqual({
content_type: 'article',
'fields.trackCategory': 'Development',
limit: 3,
order: '-sys.createdAt',
select: EXPECTED_SELECT,
});
});

test('passes a projected article and resolved Asset file to Article small', () => {
const trackCardsRenderer = new Renderer();
trackCardsRenderer.render(
<TrackCards track="Development" theme={{ trackCards: 'cards' }} />,
);
const trackCardsLoader = trackCardsRenderer.getRenderOutput();
const article = {
fields: {
commentsCount: 2,
contentAuthor: [{ sys: { id: 'author-id', linkType: 'Entry', type: 'Link' } }],
creationDate: '2026-08-12',
featuredImage: { sys: { id: 'asset-id', linkType: 'Asset', type: 'Link' } },
readTime: '5 min',
slug: 'projected-article',
tags: ['Payload'],
title: 'Projected article',
upvotes: 3,
},
sys: { id: 'article-id', type: 'Entry' },
};
const cards = trackCardsLoader.props.render({
entries: { items: { 'article-id': article } },
});

const articleLoaderRenderer = new Renderer();
articleLoaderRenderer.render(cards.props.children[0]);
const articleLoader = articleLoaderRenderer.getRenderOutput();
expect(articleLoader.props.entryIds).toBe('article-id');

const articleAssetsRenderer = new Renderer();
articleAssetsRenderer.render(articleLoader.props.render({
entries: { items: { 'article-id': article } },
}));
const assetLoader = articleAssetsRenderer.getRenderOutput();
expect(assetLoader.props.assetIds).toBe('asset-id');

const articleCard = assetLoader.props.render({
assets: {
items: {
'asset-id': {
fields: {
file: { url: '//assets.topcoder-dev.com/media/contentful/projected.png' },
},
},
},
},
});
expect(articleCard.props.article.title).toBe('Projected article');
expect(articleCard.props.featuredImage.file.url)
.toBe('//assets.topcoder-dev.com/media/contentful/projected.png');
expect(articleCard.props.themeName).toBe('Article small');
});
19 changes: 18 additions & 1 deletion __tests__/shared/containers/TopcoderHeader.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import _ from 'lodash';
import Container from 'containers/TopcoderHeader';
import Container, { TopcoderHeader } from 'containers/TopcoderHeader';
import React from 'react';
import R from 'react-test-renderer/shallow';
import { config } from 'topcoder-react-utils';

const mockState = {
auth: {
Expand All @@ -27,3 +28,19 @@ test('Matches shallow snapshot', () => {
));
expect(r.getRenderOutput()).toMatchSnapshot();
});

test('Passes the configured universal navigation URL to the navigation loader', () => {
const r = new R();
r.render((
<TopcoderHeader
location={{
href: 'https://www.topcoder-dev.com/challenges/challenge-id/submit',
pathname: '/challenges/challenge-id/submit',
search: '',
}}
/>
));

expect(r.getRenderOutput().props.children.props.uniNavUrl)
.toBe(config.UNIVERSAL_NAV_URL);
});
64 changes: 64 additions & 0 deletions __tests__/shared/routes/TopcoderRoutes.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import Renderer from 'react-test-renderer/shallow';
import { Route, Switch, matchPath } from 'react-router-dom';
import { config } from 'topcoder-react-utils';

import ContentfulRoute from 'components/Contentful/Route';
import Footer from 'components/TopcoderFooter';
import Header from 'containers/TopcoderHeader';
import EDUHome from 'routes/EDUHome';
import EDUSearch from 'routes/EDUSearch';
import EDUTracks from 'routes/EDUTracks';
import Topcoder from 'routes/Topcoder/Routes';

test('matches exact Thrive routes before the generic root Contentful route', () => {
const renderer = new Renderer();
renderer.render(<Topcoder />);

const output = renderer.getRenderOutput();
const children = React.Children.toArray(output.props.children);
const routeSwitch = children[1];
const routes = React.Children.toArray(routeSwitch.props.children);
const contentfulRouteIndex = routes.findIndex(route => route.type === ContentfulRoute);
const expectedRoutes = [
{
component: EDUHome,
path: config.TC_EDU_BASE_PATH,
pathname: config.TC_EDU_BASE_PATH,
},
{
component: EDUTracks,
path: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_TRACKS_PATH}`,
pathname: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_TRACKS_PATH}`,
},
{
component: EDUSearch,
path: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_SEARCH_PATH}`,
pathname: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_SEARCH_PATH}`,
},
{
path: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_ARTICLES_PATH}/:articleTitle`,
pathname: `${config.TC_EDU_BASE_PATH}${config.TC_EDU_ARTICLES_PATH}/routing-test`,
},
];

expect(children[0].type).toBe(Header);
expect(routeSwitch.type).toBe(Switch);
expect(children[2].type).toBe(Footer);
expect(contentfulRouteIndex).toBeGreaterThan(-1);

expectedRoutes.forEach((expectedRoute) => {
const routeIndex = routes.findIndex(route => (
route.type === Route && route.props.path === expectedRoute.path
));
const route = routes[routeIndex];

expect(routeIndex).toBeGreaterThan(-1);
expect(routeIndex).toBeLessThan(contentfulRouteIndex);
expect(route.props.exact).toBe(true);
expect(matchPath(expectedRoute.pathname, route.props)).not.toBeNull();
if (expectedRoute.component) {
expect(route.props.component).toBe(expectedRoute.component);
}
});
});
9 changes: 9 additions & 0 deletions config/custom-environment-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,23 @@ module.exports = {
SERVER_API_KEY: 'SERVER_API_KEY',

URL: {
CMS_ASSETS: 'PAYLOAD_CMS_ASSET_URL',
COMMUNITY_APP: 'COMMUNITY_APP_URL',
EMAIL_VERIFY_URL: 'EMAIL_VERIFY_URL',
},

SECRET: {
CONTENTFUL: {
MANAGEMENT_TOKEN: 'CONTENTFUL_MANAGEMENT_TOKEN',
PAYLOAD_VOTE_API_URL: 'CONTENTFUL_PAYLOAD_VOTE_API_URL',
PAYLOAD_MANAGEMENT_API_KEY: 'CONTENTFUL_PAYLOAD_MANAGEMENT_API_KEY',
default: {
SPACE_ID: 'CONTENTFUL_SPACE_ID',
master: {
CDN_API_KEY: 'CONTENTFUL_CDN_API_KEY',
PREVIEW_API_KEY: 'CONTENTFUL_PREVIEW_API_KEY',
CDN_API_HOST: 'CONTENTFUL_CDN_API_HOST',
PREVIEW_API_HOST: 'CONTENTFUL_PREVIEW_API_HOST',
},
},
zurich: {
Expand All @@ -56,13 +61,17 @@ module.exports = {
master: {
CDN_API_KEY: 'CONTENTFUL_TOPGEAR_CDN_API_KEY',
PREVIEW_API_KEY: 'CONTENTFUL_TOPGEAR_PREVIEW_API_KEY',
CDN_API_HOST: 'CONTENTFUL_TOPGEAR_CDN_API_HOST',
PREVIEW_API_HOST: 'CONTENTFUL_TOPGEAR_PREVIEW_API_HOST',
},
},
EDU: {
SPACE_ID: 'CONTENTFUL_EDU_SPACE_ID',
master: {
CDN_API_KEY: 'CONTENTFUL_EDU_CDN_API_KEY',
PREVIEW_API_KEY: 'CONTENTFUL_EDU_PREVIEW_API_KEY',
CDN_API_HOST: 'CONTENTFUL_EDU_CDN_API_HOST',
PREVIEW_API_HOST: 'CONTENTFUL_EDU_PREVIEW_API_HOST',
},
},
comcast: {
Expand Down
11 changes: 11 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ module.exports = {
/* This is the same value as above, but it is used by topcoder-react-lib,
* as a more verbose name for the param. */
COMMUNITY_APP: 'https://community-app.topcoder-dev.com',
CMS_ASSETS: 'https://assets.topcoder-dev.com',
CHALLENGES_URL: 'https://www.topcoder-dev.com/challenges',
COPILOTS_URL: 'https://copilots.topcoder-dev.com',
ENGAGEMENTS_APP: 'https://engagements.topcoder-dev.com',
Expand Down Expand Up @@ -211,18 +212,26 @@ module.exports = {
DEFAULT_SPACE_NAME: 'default',
DEFAULT_ENVIRONMENT: 'master',
MANAGEMENT_TOKEN: '', // Personal Access Token to use the Content Management API
/* Optional Payload write-through endpoint. When unset, article votes
* continue to use the Contentful Management API. */
PAYLOAD_VOTE_API_URL: '',
PAYLOAD_MANAGEMENT_API_KEY: '',
default: { // Human-readable name of space
SPACE_ID: '',
master: { // Name of an environment
CDN_API_KEY: '',
PREVIEW_API_KEY: '',
CDN_API_HOST: '',
PREVIEW_API_HOST: '',
},
},
EDU: {
SPACE_ID: '',
master: {
CDN_API_KEY: '',
PREVIEW_API_KEY: '',
CDN_API_HOST: '',
PREVIEW_API_HOST: '',
},
},
/* Space for expert communities. */
Expand All @@ -239,6 +248,8 @@ module.exports = {
master: {
CDN_API_KEY: '',
PREVIEW_API_KEY: '',
CDN_API_HOST: '',
PREVIEW_API_HOST: '',
},
},
comcast: {
Expand Down
1 change: 1 addition & 0 deletions config/production.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ module.exports = {
/* This is the same value as above, but it is used by topcoder-react-lib,
* as a more verbose name for the param. */
COMMUNITY_APP: 'https://community-app.topcoder.com',
CMS_ASSETS: 'https://assets.topcoder.com',
CHALLENGES_URL: 'https://www.topcoder.com/challenges',
COPILOTS_URL: 'https://copilots.topcoder.com',
ENGAGEMENTS_APP: 'https://engagements.topcoder.com',
Expand Down
Loading
Loading