Skip to content

Commit 1c70c21

Browse files
Initial commit
0 parents  commit 1c70c21

File tree

19 files changed

+6890
-0
lines changed

19 files changed

+6890
-0
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.idea
2+
node_modules
3+
mock-api/node_modules
4+
*.log
5+
.DS_Store
6+
.env
7+
coverage
8+
.nyc_output

README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Topcoder Challenge Resources Processor
2+
3+
## Dependencies
4+
5+
- nodejs https://nodejs.org/en/ (v10+)
6+
- Kafka
7+
8+
## Configuration
9+
10+
Configuration for the processor is at `config/default.js` and `config/production.js`.
11+
The following parameters can be set in config files or in env variables:
12+
13+
- DISABLE_LOGGING: whether to disable logging, default is false
14+
- LOG_LEVEL: the log level, default value: 'debug'
15+
- AUTH0_URL: AUTH0 URL, used to get M2M token
16+
- AUTH0_AUDIENCE: AUTH0 audience, used to get M2M token, default value is 'https://www.topcoder-dev.com'
17+
- TOKEN_CACHE_TIME: AUTH0 token cache time, used to get M2M token
18+
- AUTH0_PROXY_SERVER_URL: Auth0 proxy server url, used to get TC M2M token
19+
- AUTH0_CLIENT_ID: AUTH0 client id, used to get M2M token
20+
- AUTH0_CLIENT_SECRET: AUTH0 client secret, used to get M2M token
21+
- KAFKA_URL: comma separated Kafka hosts, default value: 'localhost:9092'
22+
- KAFKA_GROUP_ID: the Kafka group id, default value: 'challenge-resources-processor'
23+
- KAFKA_CLIENT_CERT: Kafka connection certificate, optional, default value is undefined;
24+
if not provided, then SSL connection is not used, direct insecure connection is used;
25+
if provided, it can be either path to certificate file or certificate content
26+
- KAFKA_CLIENT_CERT_KEY: Kafka connection private key, optional, default value is undefined;
27+
if not provided, then SSL connection is not used, direct insecure connection is used;
28+
if provided, it can be either path to private key file or private key content
29+
- KAFKA_TOPIC: Kafka topic to listen, default value is 'challenge.notification.create'
30+
- REQUEST_TIMEOUT: superagent request timeout in milliseconds, default value is 20000
31+
- RESOURCE_ROLE_ID: the challenge member resource role id
32+
- GET_PROJECT_API_BASE: get project API base URL, default value is mock API 'http://localhost:4000/v4/projects'
33+
- SEARCH_MEMBERS_API_BASE: search members API base URL, default value is 'https://api.topcoder.com/v3/members/_search'
34+
- CREATE_RESOURCE_API: create resource API URL, default value is mock API 'http://localhost:4000/v5/resources'
35+
36+
37+
Set the following environment variables so that the app can get TC M2M token (use 'set' insted of 'export' for Windows OS):
38+
39+
- export AUTH0_CLIENT_ID=EkE9qU3Ey6hdJwOsF1X0duwskqcDuElW
40+
- export AUTH0_CLIENT_SECRET=Iq7REiEacFmepPh0UpKoOmc6u74WjuoJriLayeVnt311qeKNBvhRNBe9BZ8WABYk
41+
- export AUTH0_URL=https://topcoder-dev.auth0.com/oauth/token
42+
- export AUTH0_AUDIENCE=https://m2m.topcoder-dev.com/
43+
- export TOKEN_CACHE_TIME=90
44+
45+
Also note that there is a `/health` endpoint that checks for the health of the app. This sets up an expressjs server and listens on the environment variable `PORT`. It's not part of the configuration file and needs to be passed as an environment variable
46+
47+
48+
Configuration for the tests is at `config/test.js`, only add such new configurations if different than `config/default.js`:
49+
- WAIT_TIME: wait time used in test, default is 2000 or 2 seconds
50+
51+
## Local Kafka setup
52+
53+
- `http://kafka.apache.org/quickstart` contains details to setup and manage Kafka server,
54+
below provides details to setup Kafka server in Mac, Windows will use bat commands in bin/windows instead
55+
- download kafka at `https://www.apache.org/dyn/closer.cgi?path=/kafka/1.1.0/kafka_2.11-1.1.0.tgz`
56+
- extract out the doanlowded tgz file
57+
- go to extracted directory kafka_2.11-0.11.0.1
58+
- start ZooKeeper server:
59+
`bin/zookeeper-server-start.sh config/zookeeper.properties`
60+
- use another terminal, go to same directory, start the Kafka server:
61+
`bin/kafka-server-start.sh config/server.properties`
62+
- note that the zookeeper server is at localhost:2181, and Kafka server is at localhost:9092
63+
- use another terminal, go to same directory, create topic:
64+
`bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic challenge.notification.create`
65+
- verify that the topics are created:
66+
`bin/kafka-topics.sh --list --zookeeper localhost:2181`,
67+
it should list out the created topic
68+
- run the producer and then write some message into the console to send to the `challenge.notification.create` topic:
69+
`bin/kafka-console-producer.sh --broker-list localhost:9092 --topic challenge.notification.create`
70+
in the console, write message, one message per line:
71+
`{ "topic": "challenge.notification.create", "originator": "challenge-api", "timestamp": "2019-02-16T00:00:00", "mime-type": "application/json", "payload": { "id": "5505f779-b28b-428f-888b-e523b443f3ea", "typeId": "7705f779-b28b-428f-888b-e523b443f3ea", "track": "code", "name": "test", "description": "desc", "timelineTemplateId": "8805f779-b28b-428f-888b-e523b443f3ea", "phases": [{ "id": "8805f779-b28b-428f-888b-e523b443f3eb", "name": "phase", "isActive": true, "duration": 1000 }], "prizeSets": [{ "type": "Challenge prizes", "prizes": [{ "type": "1st", "value": 600 }] }], "reviewType": "community", "tags": ["tag1"], "projectId": 30055214, "startDate": "2019-02-19T00:00:00", "status": "Draft", "created": "2019-02-16T00:00:00", "createdBy": "tester" } }`
72+
- optionally, use another terminal, go to same directory, start a consumer to view the messages:
73+
`bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic challenge.notification.create --from-beginning`
74+
75+
76+
## Local deployment
77+
78+
- start mock-api, go to `mock-api` folder, run `npm i` and `npm start`, mock api is running at `http://localhost:4000`
79+
- go to project root folder, install dependencies `npm i`
80+
- run code lint check `npm run lint`, running `npm run lint:fix` can fix some lint errors
81+
- start processor app `npm start`
82+
83+
84+
## Unit Tests and E2E Tests
85+
86+
Before running tests, setup and start kafka server, start the mock API, but do not start the processor app.
87+
88+
- run `npm run test` to execute unit tests.
89+
- run `npm run test:cov` to execute unit tests and generate coverage report.
90+
- run `npm run e2e` to execute e2e tests.
91+
- run `npm run e2e:cov` to execute e2e tests and generate coverage report.
92+
93+
94+
## Verification
95+
96+
- setup and start kafka server, start mock API, start processor app
97+
- start kafka-console-producer to write messages to `challenge.notification.create` topic:
98+
`bin/kafka-console-producer.sh --broker-list localhost:9092 --topic challenge.notification.create`
99+
- write message:
100+
`{ "topic": "challenge.notification.create", "originator": "challenge-api", "timestamp": "2019-02-16T00:00:00", "mime-type": "application/json", "payload": { "id": "5505f779-b28b-428f-888b-e523b443f3ea", "typeId": "7705f779-b28b-428f-888b-e523b443f3ea", "track": "code", "name": "test", "description": "desc", "timelineTemplateId": "8805f779-b28b-428f-888b-e523b443f3ea", "phases": [{ "id": "8805f779-b28b-428f-888b-e523b443f3eb", "name": "phase", "isActive": true, "duration": 1000 }], "prizeSets": [{ "type": "Challenge prizes", "prizes": [{ "type": "1st", "value": 600 }] }], "reviewType": "community", "tags": ["tag1"], "projectId": 30055214, "startDate": "2019-02-19T00:00:00", "status": "Draft", "created": "2019-02-16T00:00:00", "createdBy": "tester" } }`
101+
102+
- you will see app logging:
103+
```bash
104+
info: Process message of challenge id 5505f779-b28b-428f-888b-e523b443f3ea and project id 30055214
105+
info: Found member ids [40152933, 40141336] of project id 30055214
106+
info: Created resource: {
107+
"challengeId": "5505f779-b28b-428f-888b-e523b443f3ea",
108+
"memberHandle": "lordofparadox",
109+
"roleId": "6605f779-b28b-428f-888b-e523b443f3ea",
110+
"id": "d4ac5715-b1ed-430b-86ca-56d022c97ce9",
111+
"memberId": "9e2ad182-d4f7-4c4d-9421-29bc8bd56dfb",
112+
"created": "2019-08-20T23:02:45.818Z",
113+
"createdBy": "mock-api"
114+
}
115+
info: Created resource: {
116+
"challengeId": "5505f779-b28b-428f-888b-e523b443f3ea",
117+
"memberHandle": "SethHafferkamp",
118+
"roleId": "6605f779-b28b-428f-888b-e523b443f3ea",
119+
"id": "8a835c7e-5a90-47f1-a020-c95c415e32db",
120+
"memberId": "980bd2a6-b4b6-4253-87e7-67cb931a76a2",
121+
"created": "2019-08-20T23:02:45.822Z",
122+
"createdBy": "mock-api"
123+
}
124+
info: Successfully processed message of challenge id 5505f779-b28b-428f-888b-e523b443f3ea and project id 30055214
125+
```
126+
127+
128+
- you may write invalid messages like:
129+
`{ "topic": "challenge.notification.create", "originator": "challenge-api", "timestamp": "2019-02-16T00:00:00", "mime-type": "application/json", "payload": { "id": "abc", "typeId": "7705f779-b28b-428f-888b-e523b443f3ea", "track": "code", "name": "test", "description": "desc", "timelineTemplateId": "8805f779-b28b-428f-888b-e523b443f3ea", "phases": [{ "id": "8805f779-b28b-428f-888b-e523b443f3eb", "name": "phase", "isActive": true, "duration": 1000 }], "prizeSets": [{ "type": "Challenge prizes", "prizes": [{ "type": "1st", "value": 600 }] }], "reviewType": "community", "tags": ["tag1"], "projectId": 30055214, "startDate": "2019-02-19T00:00:00", "status": "Draft", "created": "2019-02-16T00:00:00", "createdBy": "tester" } }`
130+
131+
`{ "topic": "challenge.notification.create", "originator": "challenge-api", "timestamp": "2019-02-16T00:00:00", "mime-type": "application/json", "payload": { "id": "5505f779-b28b-428f-888b-e523b443f3ea", "typeId": "7705f779-b28b-428f-888b-e523b443f3ea", "track": "code", "name": "test", "description": "desc", "timelineTemplateId": "8805f779-b28b-428f-888b-e523b443f3ea", "phases": [{ "id": "8805f779-b28b-428f-888b-e523b443f3eb", "name": "phase", "isActive": true, "duration": 1000 }], "prizeSets": [{ "type": "Challenge prizes", "prizes": [{ "type": "1st", "value": 600 }] }], "reviewType": "community", "tags": ["tag1"], "projectId": 30055214, "startDate": "2019-02-19T00:00:00", "status": "Draft", "created": "abc", "createdBy": "tester" } }`
132+
133+
`{ [ { abc`
134+
- then in the app console, you will see error messages
135+
136+
137+
- to test the health check API, start the processor, then browse `http://localhost:3000/health` in a browser,
138+
and you will see result `{"checksRun":1}`, you may change the health check API port by setting `PORT` environment variable
139+

config/default.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* The default configuration file.
3+
*/
4+
5+
module.exports = {
6+
DISABLE_LOGGING: process.env.DISABLE_LOGGING
7+
? process.env.DISABLE_LOGGING.toLowerCase() === 'true' : false, // If true, logging will be disabled
8+
LOG_LEVEL: process.env.LOG_LEVEL || 'debug',
9+
10+
// used to get M2M token
11+
AUTH0_URL: process.env.AUTH0_URL,
12+
AUTH0_AUDIENCE: process.env.AUTH0_AUDIENCE || 'https://www.topcoder-dev.com',
13+
TOKEN_CACHE_TIME: process.env.TOKEN_CACHE_TIME,
14+
AUTH0_PROXY_SERVER_URL: process.env.AUTH0_PROXY_SERVER_URL,
15+
AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
16+
AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
17+
18+
KAFKA_URL: process.env.KAFKA_URL || 'localhost:9092',
19+
KAFKA_GROUP_ID: process.env.KAFKA_GROUP_ID || 'challenge-resources-processor',
20+
// below are used for secure Kafka connection, they are optional
21+
// for the local Kafka, they are not needed
22+
KAFKA_CLIENT_CERT: process.env.KAFKA_CLIENT_CERT,
23+
KAFKA_CLIENT_CERT_KEY: process.env.KAFKA_CLIENT_CERT_KEY,
24+
KAFKA_TOPIC: process.env.KAFKA_TOPIC || 'challenge.notification.create',
25+
26+
// superagent request timeout in milliseconds
27+
REQUEST_TIMEOUT: process.env.REQUEST_TIMEOUT ? Number(process.env.REQUEST_TIMEOUT) : 20000,
28+
29+
RESOURCE_ROLE_ID: process.env.RESOURCE_ROLE_ID || '6605f779-b28b-428f-888b-e523b443f3ea',
30+
31+
GET_PROJECT_API_BASE: process.env.GET_PROJECT_API_BASE || 'http://localhost:4000/v4/projects',
32+
SEARCH_MEMBERS_API_BASE: process.env.SEARCH_MEMBERS_API_BASE || 'https://api.topcoder.com/v3/members/_search',
33+
CREATE_RESOURCE_API: process.env.CREATE_RESOURCE_API || 'http://localhost:4000/v5/resources'
34+
}

config/production.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Production configuration file
3+
*/
4+
5+
module.exports = {
6+
LOG_LEVEL: process.env.LOG_LEVEL || 'info'
7+
}

config/test.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The test configuration.
3+
*/
4+
5+
module.exports = {
6+
WAIT_TIME: 2000
7+
}

mock-api/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Mock TC APIs
2+
3+
It provides a mock API to create TC resource, and a mock API to get project by id.
4+
5+
## Dependencies
6+
- NodeJS https://nodejs.org/en/ (v10+)
7+
8+
9+
## Configuration
10+
Configuration is at `config/default.js`, you may also set env variables.
11+
There are following config params:
12+
- `PORT`: REST app port, default value is 4000
13+
14+
15+
## Local deployment
16+
- Install dependencies `npm i`
17+
- Start app `npm start`
18+
- App is running at `http://localhost:4000`
19+

mock-api/app.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* The application entry point for mock TC APIs
3+
*/
4+
5+
const express = require('express')
6+
const bodyParser = require('body-parser')
7+
const cors = require('cors')
8+
const config = require('config')
9+
const winston = require('winston')
10+
const uuid = require('uuid/v4')
11+
12+
const app = express()
13+
app.set('port', config.PORT)
14+
15+
app.use(cors())
16+
app.use(bodyParser.json())
17+
app.use(bodyParser.urlencoded({ extended: true }))
18+
19+
// create resource
20+
app.post('/v5/resources', (req, res) => {
21+
winston.info(`Create resource: ${JSON.stringify(req.body, null, 4)}`)
22+
23+
if (req.body.challengeId === '55ba42a4-f7f9-4f1e-bf81-a8553ba53778') {
24+
winston.info('Failed to create resource.')
25+
res.status(500).json({ message: 'there is error' })
26+
return
27+
}
28+
29+
const result = req.body
30+
result.id = uuid()
31+
result.memberId = uuid()
32+
result.created = new Date()
33+
result.createdBy = 'mock-api'
34+
35+
winston.info(`Created resource: ${JSON.stringify(result, null, 4)}`)
36+
res.json(result)
37+
})
38+
39+
// get project by id
40+
app.get('/v4/projects/:projectId', (req, res) => {
41+
const projectId = req.params.projectId
42+
winston.info(`Get project of id: ${projectId}`)
43+
44+
let result
45+
if (projectId === '123') {
46+
result = {
47+
id: '6eba42a4-f7f9-4f1e-bf81-a8553ba5372a',
48+
version: 'v4',
49+
result: {
50+
success: false,
51+
status: 404,
52+
content: 'it is not found'
53+
}
54+
}
55+
} else {
56+
result = {
57+
id: '7eba42a4-f7f9-4f1e-bf81-a8553ba5371a',
58+
version: 'v4',
59+
result: {
60+
success: true,
61+
status: 200,
62+
content: {
63+
id: projectId,
64+
directProjectId: null,
65+
billingAccountId: null,
66+
name: 'Alpha',
67+
description: 'And away we go.',
68+
external: null,
69+
bookmarks: null,
70+
estimatedPrice: '0.00',
71+
actualPrice: null,
72+
terms: [],
73+
type: 'app_dev',
74+
status: 'paused',
75+
details: {
76+
fontIds: [],
77+
offlineAccess: null,
78+
deviceIds: ['PHONE'],
79+
offlineAccessComment: null,
80+
orientationIds: ['PORTRAIT'],
81+
iconsetIds: [],
82+
usesPersonalInformation: null,
83+
colorSwatchIds: [],
84+
modelType: 'app-project',
85+
version: null,
86+
securityLevel: null,
87+
features: '[ ]',
88+
apiIntegration: null,
89+
designNotes: null
90+
},
91+
challengeEligibility: null,
92+
cancelReason: null,
93+
templateId: null,
94+
createdAt: '2016-04-27T22:15:04.285Z',
95+
updatedAt: '2016-10-01T17:33:44.000Z',
96+
deletedBy: null,
97+
createdBy: 40141336,
98+
updatedBy: 40152933,
99+
version: 'v2',
100+
lastActivityAt: '2016-10-01T17:33:44.000Z',
101+
lastActivityUserId: '40152933',
102+
members: [{
103+
id: 995,
104+
userId: 40152933,
105+
role: 'manager',
106+
isPrimary: true,
107+
createdAt: '2016-10-01T17:33:32.000Z',
108+
updatedAt: '2016-10-01T17:33:32.000Z',
109+
deletedBy: null,
110+
createdBy: 40152933,
111+
updatedBy: 40152933,
112+
projectId
113+
}, {
114+
id: 998,
115+
userId: 40141336,
116+
role: 'manager',
117+
isPrimary: true,
118+
createdAt: '2016-10-02T17:33:32.000Z',
119+
updatedAt: '2016-10-02T17:33:32.000Z',
120+
deletedBy: null,
121+
createdBy: 40152933,
122+
updatedBy: 40152933,
123+
projectId
124+
}],
125+
attachments: [],
126+
invites: [],
127+
scopeChangeRequests: []
128+
},
129+
metadata: {
130+
totalCount: 1
131+
}
132+
}
133+
}
134+
}
135+
136+
winston.info(`Result: ${JSON.stringify(result, null, 4)}`)
137+
res.json(result)
138+
})
139+
140+
app.use((req, res) => {
141+
res.status(404).json({ error: 'route not found' })
142+
})
143+
144+
app.use((err, req, res, next) => {
145+
winston.error(err)
146+
res.status(500).json({
147+
error: err.message
148+
})
149+
})
150+
151+
app.listen(app.get('port'), '0.0.0.0', () => {
152+
winston.info(`Express server listening on port ${app.get('port')}`)
153+
})

mock-api/config/default.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* The default configuration file.
3+
*/
4+
5+
module.exports = {
6+
PORT: process.env.PORT || 4000
7+
}

0 commit comments

Comments
 (0)