mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
Merge pull request #172 from oceanprotocol/feature/reporting
Reporting data sets
This commit is contained in:
commit
b6770e68de
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,6 +12,7 @@ dist
|
|||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
|
17
README.md
17
README.md
|
@ -26,6 +26,8 @@ If you're a developer and want to contribute to, or want to utilize this marketp
|
|||
- [🏖 Remote Ocean: Pacific](#-Remote-Ocean-Pacific)
|
||||
- [🐳 Use with Barge](#-Use-with-Barge)
|
||||
- [⛵️ Environment Variables](#️-Environment-Variables)
|
||||
- [Client](#Client)
|
||||
- [Server](#Server)
|
||||
- [👩🔬 Testing](#-Testing)
|
||||
- [Unit Tests](#Unit-Tests)
|
||||
- [End-to-End Integration Tests](#End-to-End-Integration-Tests)
|
||||
|
@ -41,7 +43,7 @@ If you're a developer and want to contribute to, or want to utilize this marketp
|
|||
This repo contains a client and a server, both written in TypeScript:
|
||||
|
||||
- **client**: React app setup with [squid-js](https://github.com/oceanprotocol/squid-js), bootstrapped with [Create React App](https://github.com/facebook/create-react-app)
|
||||
- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking.
|
||||
- **server**: Node.js app, utilizing [Express](https://expressjs.com). The server provides various microservices, like remote file checking. The endpoints are documented in [server Readme](server/).
|
||||
|
||||
To spin up both, the client and the server in a watch mode for local development, execute:
|
||||
|
||||
|
@ -79,6 +81,8 @@ Modify `./client/src/config.ts` or set environment variables to use those local
|
|||
|
||||
### ⛵️ Environment Variables
|
||||
|
||||
#### Client
|
||||
|
||||
The `./client/src/config.ts` file is setup to prioritize environment variables for setting each Ocean component endpoint.
|
||||
|
||||
By setting environment variables, you can easily switch between Ocean networks the commons client connects to, without directly modifying `./client/src/config.ts`. This is helpful e.g. for local development so you don't accidentially commit changes to the config file.
|
||||
|
@ -92,6 +96,17 @@ cp client/.env.local.example client/.env.local
|
|||
vi client/.env.local
|
||||
```
|
||||
|
||||
#### Server
|
||||
|
||||
The server uses its own environment variables too:
|
||||
|
||||
```bash
|
||||
cp server/.env.example server/.env
|
||||
|
||||
# edit variables
|
||||
vi server/.env
|
||||
```
|
||||
|
||||
## 👩🔬 Testing
|
||||
|
||||
Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library) for unit testing, and [Cypress](https://www.cypress.io) for integration testing.
|
||||
|
|
|
@ -52,3 +52,5 @@ REACT_APP_BRIZO_ADDRESS="0x008c25ed3594e094db4592f4115d5fa74c4f41ea"
|
|||
# REACT_APP_SECRET_STORE_URI="http://localhost:12001"
|
||||
# REACT_APP_FAUCET_URI="http://localhost:3001"
|
||||
# REACT_APP_BRIZO_ADDRESS="0x00bd138abd70e2f00903268f3db08f2d25677c9e"
|
||||
|
||||
REACT_APP_REPORT_EMAIL="test@example.com"
|
||||
|
|
25
client/package-lock.json
generated
25
client/package-lock.json
generated
|
@ -1740,6 +1740,15 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-modal": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.8.2.tgz",
|
||||
"integrity": "sha512-/Drs+XfHg9M60fy2Q63UUlhECXSNknDu3tnwFnbOhcdDjq03VD3hLCfv3X+BBzRqgu4TOu+TwEwBhgI8qdVAAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-paginate": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-paginate/-/react-paginate-6.2.1.tgz",
|
||||
|
@ -13136,6 +13145,11 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
|
||||
},
|
||||
"react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-markdown": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-4.1.0.tgz",
|
||||
|
@ -13150,6 +13164,17 @@
|
|||
"xtend": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"react-modal": {
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.8.2.tgz",
|
||||
"integrity": "sha512-wxNk94wy/DMh2LyJa8K+LyOQDhQfhKuBrZ4SxS091p75cpW+STfY+9GpAuvl6P6Yt2r/+wxYH8Z3G5Ww/L8Tiw==",
|
||||
"requires": {
|
||||
"exenv": "^1.2.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react-lifecycles-compat": "^3.0.0",
|
||||
"warning": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"react-moment": {
|
||||
"version": "0.9.2",
|
||||
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-0.9.2.tgz",
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"react-ga": "^2.6.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-markdown": "^4.1.0",
|
||||
"react-modal": "^3.8.2",
|
||||
"react-moment": "^0.9.2",
|
||||
"react-paginate": "^6.3.0",
|
||||
"react-popper": "^1.3.3",
|
||||
|
@ -51,6 +52,7 @@
|
|||
"@types/react-dom": "^16.8.4",
|
||||
"@types/react-dotdotdot": "^1.2.0",
|
||||
"@types/react-helmet": "^5.0.8",
|
||||
"@types/react-modal": "^3.8.2",
|
||||
"@types/react-paginate": "^6.2.1",
|
||||
"@types/react-router-dom": "^4.3.4",
|
||||
"@types/react-transition-group": "^2.9.2",
|
||||
|
|
|
@ -12,6 +12,7 @@ interface ButtonProps {
|
|||
onClick?: any
|
||||
disabled?: boolean
|
||||
to?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export default class Button extends PureComponent<ButtonProps, any> {
|
||||
|
|
88
client/src/components/atoms/Modal.module.scss
Normal file
88
client/src/components/atoms/Modal.module.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
@import '../../styles/variables';
|
||||
|
||||
.modalOverlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba($brand-black, .7);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
animation: fadeIn .2s ease-out backwards;
|
||||
}
|
||||
|
||||
.modal {
|
||||
padding: $spacer;
|
||||
border-radius: $border-radius;
|
||||
background: $body-background;
|
||||
margin: $spacer auto;
|
||||
max-width: $break-point--small;
|
||||
position: relative;
|
||||
animation: moveUp .2s ease-out backwards;
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
padding: $spacer * 2 $spacer * 1.5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-h3;
|
||||
margin: 0;
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
font-size: $font-size-h2;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
margin-top: $spacer / 2;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
outline: 0;
|
||||
top: $spacer / 4;
|
||||
right: $spacer / 2;
|
||||
font-size: $font-size-h2;
|
||||
color: $brand-grey;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveUp {
|
||||
from {
|
||||
transform: translate3d(0, 1rem, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
17
client/src/components/atoms/Modal.test.tsx
Normal file
17
client/src/components/atoms/Modal.test.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import Modal from './Modal'
|
||||
import ReactModal from 'react-modal'
|
||||
|
||||
describe('Modal', () => {
|
||||
it('renders without crashing', () => {
|
||||
ReactModal.setAppElement(document.createElement('div'))
|
||||
|
||||
render(
|
||||
<Modal title="Hello" isOpen toggleModal={() => null}>
|
||||
Hello
|
||||
</Modal>
|
||||
)
|
||||
expect(document.querySelector('.ReactModalPortal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
51
client/src/components/atoms/Modal.tsx
Normal file
51
client/src/components/atoms/Modal.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import React from 'react'
|
||||
import ReactModal from 'react-modal'
|
||||
import styles from './Modal.module.scss'
|
||||
|
||||
if (process.env.NODE_ENV !== 'test') ReactModal.setAppElement('#root')
|
||||
|
||||
const Modal = ({
|
||||
title,
|
||||
description,
|
||||
isOpen,
|
||||
toggleModal,
|
||||
children,
|
||||
onAfterOpen,
|
||||
onRequestClose,
|
||||
...props
|
||||
}: {
|
||||
title: string
|
||||
description?: string
|
||||
isOpen: boolean
|
||||
toggleModal: () => void
|
||||
children: any
|
||||
onAfterOpen?: () => void
|
||||
onRequestClose?: () => void
|
||||
}) => {
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onAfterOpen={onAfterOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
contentLabel={title}
|
||||
className={styles.modal}
|
||||
overlayClassName={styles.modalOverlay}
|
||||
{...props}
|
||||
>
|
||||
<button className={styles.close} onClick={toggleModal}>
|
||||
×
|
||||
</button>
|
||||
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>{title}</h2>
|
||||
{description && (
|
||||
<p className={styles.description}>{description}</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{children}
|
||||
</ReactModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default Modal
|
|
@ -5,6 +5,7 @@ import Markdown from '../../atoms/Markdown'
|
|||
import CategoryLink from '../../atoms/CategoryLink'
|
||||
import styles from './AssetDetails.module.scss'
|
||||
import AssetFilesDetails from './AssetFilesDetails'
|
||||
import Report from './Report'
|
||||
|
||||
interface AssetDetailsProps {
|
||||
metadata: MetaData
|
||||
|
@ -58,6 +59,8 @@ export default class AssetDetails extends PureComponent<AssetDetailsProps> {
|
|||
/>
|
||||
)}
|
||||
|
||||
<Report did={ddo.id} title={metadata.base.name} />
|
||||
|
||||
<div className={styles.metaFixed}>
|
||||
<h2
|
||||
className={styles.metaFixedTitle}
|
||||
|
@ -97,10 +100,6 @@ export default class AssetDetails extends PureComponent<AssetDetailsProps> {
|
|||
files={base.files ? base.files : []}
|
||||
ddo={ddo}
|
||||
/>
|
||||
|
||||
{/* <pre>
|
||||
<code>{JSON.stringify(metadata, null, 2)}</code>
|
||||
</pre> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -146,6 +146,7 @@ export default class AssetFile extends PureComponent<
|
|||
// weird 0 hack so TypeScript is happy
|
||||
onClick={() => this.purchaseAsset(ddo, index || 0)}
|
||||
disabled={!isLogged || !isOceanNetwork}
|
||||
name="Download"
|
||||
>
|
||||
Get file
|
||||
</Button>
|
||||
|
|
49
client/src/components/templates/Asset/Report.module.scss
Normal file
49
client/src/components/templates/Asset/Report.module.scss
Normal file
|
@ -0,0 +1,49 @@
|
|||
@import '../../../styles/variables';
|
||||
|
||||
.actions {
|
||||
text-align: right;
|
||||
margin-top: $spacer;
|
||||
}
|
||||
|
||||
.openLink {
|
||||
margin: 0;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: $brand-white;
|
||||
padding: $spacer;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-base;
|
||||
margin-top: 0;
|
||||
margin-bottom: $spacer / 8;
|
||||
}
|
||||
|
||||
p {
|
||||
border-bottom: 1px solid $brand-grey-lighter;
|
||||
padding-bottom: $spacer / 2;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
color: $brand-grey-light;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: $red;
|
||||
padding: $spacer / 2;
|
||||
text-align: center;
|
||||
color: $brand-white;
|
||||
border-radius: $border-radius;
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
|
||||
.success {
|
||||
composes: error;
|
||||
background: $green;
|
||||
}
|
144
client/src/components/templates/Asset/Report.tsx
Normal file
144
client/src/components/templates/Asset/Report.tsx
Normal file
|
@ -0,0 +1,144 @@
|
|||
import React, { PureComponent, ChangeEvent } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import Modal from '../../atoms/Modal'
|
||||
import styles from './Report.module.scss'
|
||||
import Button from '../../atoms/Button'
|
||||
import Input from '../../atoms/Form/Input'
|
||||
import Form from '../../atoms/Form/Form'
|
||||
import { serviceUri } from '../../../config'
|
||||
import Spinner from '../../atoms/Spinner'
|
||||
|
||||
export default class Report extends PureComponent<
|
||||
{ did: string; title: string },
|
||||
{
|
||||
isModalOpen: boolean
|
||||
comment: string
|
||||
message: string
|
||||
isSending: boolean
|
||||
hasError?: boolean
|
||||
hasSuccess?: boolean
|
||||
}
|
||||
> {
|
||||
public state = {
|
||||
isModalOpen: false,
|
||||
comment: '',
|
||||
message: 'Sending...',
|
||||
isSending: false,
|
||||
hasError: false,
|
||||
hasSuccess: false
|
||||
}
|
||||
|
||||
// for canceling axios requests
|
||||
public signal = axios.CancelToken.source()
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.signal.cancel()
|
||||
}
|
||||
|
||||
private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
comment: event.target.value
|
||||
})
|
||||
}
|
||||
|
||||
private toggleModal = () => {
|
||||
this.setState({ isModalOpen: !this.state.isModalOpen })
|
||||
}
|
||||
|
||||
private sendEmail = async (event: Event) => {
|
||||
event.preventDefault()
|
||||
this.setState({ isSending: true })
|
||||
|
||||
const msg = {
|
||||
to: process.env.REACT_APP_REPORT_EMAIL,
|
||||
from: 'info@oceanprotocol.com',
|
||||
subject: `[Report] ${this.props.title}`,
|
||||
html: `<p>The following data set was reported:</p><p><strong>${this.props.title}</strong><br /><a style="color:#ff4092;text-decoration:none" href="https://commons.oceanprotocol.com/asset/${this.props.did}"><code>${this.props.did}</code></a></p><blockquote><em>${this.state.comment}</em></blockquote>`
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
url: `${serviceUri}/api/v1/report`,
|
||||
data: { msg },
|
||||
cancelToken: this.signal.token
|
||||
})
|
||||
|
||||
this.setState({
|
||||
isSending: false,
|
||||
hasSuccess: true,
|
||||
message: 'Thanks for the report! We will take a look soon.'
|
||||
})
|
||||
return response.data.result
|
||||
} catch (error) {
|
||||
!axios.isCancel(error) &&
|
||||
this.setState({
|
||||
message: error.message,
|
||||
isSending: false,
|
||||
hasError: true
|
||||
}) &&
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
link
|
||||
className={styles.openLink}
|
||||
onClick={this.toggleModal}
|
||||
>
|
||||
Report Data Set
|
||||
</Button>
|
||||
<Modal
|
||||
title="Report Data Set"
|
||||
description="Found some faulty metadata, wrongly attributed data, or anything else wrong with this data set? Tell us about it and we will take a look."
|
||||
isOpen={this.state.isModalOpen}
|
||||
toggleModal={this.toggleModal}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<h3>{this.props.title}</h3>
|
||||
<p>
|
||||
<code>{this.props.did}</code>
|
||||
</p>
|
||||
|
||||
{this.state.isSending ? (
|
||||
<Spinner message={this.state.message} />
|
||||
) : this.state.hasError ? (
|
||||
<div className={styles.error}>
|
||||
{this.state.message}
|
||||
</div>
|
||||
) : this.state.hasSuccess ? (
|
||||
<div className={styles.success}>
|
||||
{this.state.message}
|
||||
</div>
|
||||
) : (
|
||||
<Form minimal>
|
||||
<Input
|
||||
type="textarea"
|
||||
name="comment"
|
||||
label="Comment"
|
||||
help="Briefly describe what is wrong with this asset. If you want to get contacted by us, add your email at the end."
|
||||
required
|
||||
value={this.state.comment}
|
||||
onChange={this.inputChange}
|
||||
rows={1}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
onClick={(e: Event) => this.sendEmail(e)}
|
||||
disabled={this.state.comment === ''}
|
||||
>
|
||||
Report Data Set
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -274,6 +274,9 @@ samp {
|
|||
font-size: $font-size-small;
|
||||
border-radius: $border-radius;
|
||||
text-shadow: none;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
|
||||
h1 &,
|
||||
h2 &,
|
||||
|
|
|
@ -3,19 +3,27 @@ context('Consume', () => {
|
|||
before(() => {
|
||||
cy.visit(`/asset/${Cypress.env('CONSUME_ASSET')}`)
|
||||
|
||||
// Wait for end of loading
|
||||
cy.get('button', { timeout: 60000 }).should('have.length', 1)
|
||||
// Alias button selector & wait for end of loading
|
||||
cy.get('button[name="Download"]', { timeout: 60000 })
|
||||
.first()
|
||||
.should('have.length', 1)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
cy.get('button[name="Download"]')
|
||||
.first()
|
||||
.as('button')
|
||||
})
|
||||
|
||||
it('Download button is clickable when user is connected.', () => {
|
||||
cy.get('button').should('not.be.disabled')
|
||||
cy.get('@button').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('Consume asset and check if there is no error', () => {
|
||||
// Click consume button
|
||||
cy.get('button').click()
|
||||
cy.get('@button').click()
|
||||
// Wait consume process to end
|
||||
cy.get('button', { timeout: 150000 }).should('contain', 'Get file')
|
||||
cy.get('@button', { timeout: 150000 }).should('contain', 'Get file')
|
||||
// check if there is no error
|
||||
cy.get('article>div').should(
|
||||
'not.contain',
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -9943,9 +9943,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
|
||||
"integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
|
||||
"integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"stylelint-config-bigchaindb": "^1.2.2",
|
||||
"stylelint-config-css-modules": "^1.4.0",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"typescript": "3.4.5"
|
||||
"typescript": "3.5.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
1
server/.env.example
Normal file
1
server/.env.example
Normal file
|
@ -0,0 +1 @@
|
|||
SENDGRID_API_KEY='xxx'
|
118
server/README.md
Normal file
118
server/README.md
Normal file
|
@ -0,0 +1,118 @@
|
|||
[![banner](https://raw.githubusercontent.com/oceanprotocol/art/master/github/repo-banner%402x.png)](https://oceanprotocol.com)
|
||||
|
||||
<h1 align="center">Commons: Server</h1>
|
||||
|
||||
This folder contains server component written in TypeScript using [Express](https://expressjs.com). The server provides various microservices.
|
||||
|
||||
- [Get Started](#Get-Started)
|
||||
- [✨ API Documentation](#-API-Documentation)
|
||||
- [Url Checker](#Url-Checker)
|
||||
- [Report](#Report)
|
||||
- [🎁 Contributing](#-Contributing)
|
||||
- [🏛 License](#-License)
|
||||
|
||||
## Get Started
|
||||
|
||||
To spin up the server in a watch mode for local development, execute:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## ✨ API Documentation
|
||||
|
||||
### Url Checker
|
||||
|
||||
Url Checker returns size and additional information about requested file. This service is used as a solution to frontend CORS restrictions.
|
||||
|
||||
**Endpoint:** POST `/api/v1/urlcheck`
|
||||
|
||||
**Request Parameters**
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://oceanprotocol.com/tech-whitepaper.pdf"
|
||||
}
|
||||
```
|
||||
|
||||
**Response: Success**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"result": {
|
||||
"found": true,
|
||||
"contentLength": "2989228",
|
||||
"contentType": "application/pdf"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response: Error**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": null
|
||||
}
|
||||
```
|
||||
|
||||
### Report
|
||||
|
||||
Report endpoints sends an email via SendGrid. Requires `SENDGRID_API_KEY` set as environment variables.
|
||||
|
||||
**Endpoint:** POST `/api/v1/report`
|
||||
|
||||
**Request Parameters**
|
||||
|
||||
```json
|
||||
{
|
||||
"msg": {
|
||||
"to": "test@example.com",
|
||||
"from": "test@example.com",
|
||||
"subject": "My Subject",
|
||||
"text": "Text",
|
||||
"html": "<strong>HTML</strong>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response: Success**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
**Response: Error**
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "error",
|
||||
"message": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
## 🎁 Contributing
|
||||
|
||||
See the page titled "[Ways to Contribute](https://docs.oceanprotocol.com/concepts/contributing/)" in the Ocean Protocol documentation.
|
||||
|
||||
## 🏛 License
|
||||
|
||||
```text
|
||||
Copyright 2019 Ocean Protocol Foundation Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
96
server/package-lock.json
generated
96
server/package-lock.json
generated
|
@ -351,6 +351,34 @@
|
|||
"@types/yargs": "^12.0.9"
|
||||
}
|
||||
},
|
||||
"@sendgrid/client": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-6.4.0.tgz",
|
||||
"integrity": "sha512-GcO+hKXMQiwN0xMGfPITArlj4Nab1vZsrsRLmsJlcXGZV1V1zQC6XuAWJv6MGDd0hr/jKaXmCJ1XMYkxIRQHFw==",
|
||||
"requires": {
|
||||
"@sendgrid/helpers": "^6.4.0",
|
||||
"@types/request": "^2.0.3",
|
||||
"request": "^2.88.0"
|
||||
}
|
||||
},
|
||||
"@sendgrid/helpers": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-6.4.0.tgz",
|
||||
"integrity": "sha512-1dDDXauArHyxwTKFFfWvQpsijmwalyLgwoQJ3FRCssFq1RfqYDgFhRg0Xs3v/IXS2jkKWePSWiPORSR4Sysdpw==",
|
||||
"requires": {
|
||||
"chalk": "^2.0.1",
|
||||
"deepmerge": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"@sendgrid/mail": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-6.4.0.tgz",
|
||||
"integrity": "sha512-pVzbqbxhZ4FUN6iSIksRLtyXRPurrcee1i0noPDStDCLlHVwUR+TofeeKIFWGpIvbbk5UR6S6iV/U5ie8Kdblw==",
|
||||
"requires": {
|
||||
"@sendgrid/client": "^6.4.0",
|
||||
"@sendgrid/helpers": "^6.4.0"
|
||||
}
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz",
|
||||
|
@ -405,8 +433,7 @@
|
|||
"@types/caseless": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
|
||||
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
|
||||
},
|
||||
"@types/compression": {
|
||||
"version": "0.0.36",
|
||||
|
@ -463,7 +490,6 @@
|
|||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz",
|
||||
"integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
|
@ -524,10 +550,9 @@
|
|||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "11.13.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.15.tgz",
|
||||
"integrity": "sha512-x6ypl5Uzly+j23hbxmMzf12Eb4lOhIEqQz0HuczpTUa1KIx1GpbN/o4E3aAED20UoEsdK0wvyY8QcffuWSLDkw==",
|
||||
"dev": true
|
||||
"version": "12.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.12.tgz",
|
||||
"integrity": "sha512-Uy0PN4R5vgBUXFoJrKryf5aTk3kJ8Rv3PdlHjl6UaX+Cqp1QE0yPQ68MPXGrZOfG7gZVNDIJZYyot0B9ubXUrQ=="
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.3",
|
||||
|
@ -539,7 +564,6 @@
|
|||
"version": "2.48.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz",
|
||||
"integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/caseless": "*",
|
||||
"@types/form-data": "*",
|
||||
|
@ -564,9 +588,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/superagent": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.1.tgz",
|
||||
"integrity": "sha512-NetXrraTWPcdGG6IwYJhJ5esUGx8AYNiozbc1ENWEsF6BsD4JmNODJczI6Rm1xFPVp6HZESds9YCfqz4zIsM6A==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.2.tgz",
|
||||
"integrity": "sha512-GISrJnl+eZSzkVdsP2bXARXaroe/qKTwl/7v/d7bHP4OhlZKKIExcvQexwTDWHGtalHSLVuM78/Ri54laoOFfQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/cookiejar": "*",
|
||||
|
@ -574,9 +598,9 @@
|
|||
}
|
||||
},
|
||||
"@types/supertest": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.7.tgz",
|
||||
"integrity": "sha512-GibTh4OTkal71btYe2fpZP/rVHIPnnUsYphEaoywVHo+mo2a/LhlOFkIm5wdN0H0DA0Hx8x+tKgCYMD9elHu5w==",
|
||||
"version": "2.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.8.tgz",
|
||||
"integrity": "sha512-wcax7/ip4XSSJRLbNzEIUVy2xjcBIZZAuSd2vtltQfRK7kxhx5WMHbLHkYdxN3wuQCrwpYrg86/9byDjPXoGMA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/superagent": "*"
|
||||
|
@ -585,8 +609,7 @@
|
|||
"@types/tough-cookie": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz",
|
||||
"integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "12.0.12",
|
||||
|
@ -681,7 +704,6 @@
|
|||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
|
@ -1117,7 +1139,6 @@
|
|||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
|
@ -1241,7 +1262,6 @@
|
|||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
|
@ -1249,8 +1269,7 @@
|
|||
"color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
|
@ -1504,6 +1523,11 @@
|
|||
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
|
||||
"dev": true
|
||||
},
|
||||
"deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
|
@ -1605,6 +1629,11 @@
|
|||
"is-obj": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
|
||||
"integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg=="
|
||||
},
|
||||
"duplexer3": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
|
||||
|
@ -1681,8 +1710,7 @@
|
|||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
|
||||
},
|
||||
"escodegen": {
|
||||
"version": "1.11.1",
|
||||
|
@ -1853,9 +1881,9 @@
|
|||
}
|
||||
},
|
||||
"express-validator": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.0.1.tgz",
|
||||
"integrity": "sha512-hrvN512QBs8zKm2vu33a0AxomZBiDl/0jAYxoq3lnGdXrbWBvObGYugt8jmdfZbzoFDlT2ZF8jhYYszyjtdOjw==",
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.1.1.tgz",
|
||||
"integrity": "sha512-AF6YOhdDiCU7tUOO/OHp2W++I3qpYX7EInMmEEcRGOjs+qoubwgc5s6Wo3OQgxwsWRGCxXlrF73SIDEmY4y3wg==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.11",
|
||||
"validator": "^11.0.0"
|
||||
|
@ -2800,8 +2828,7 @@
|
|||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.0",
|
||||
|
@ -5537,7 +5564,6 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
|
@ -5793,9 +5819,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
|
||||
"integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz",
|
||||
"integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
|
@ -6031,9 +6057,9 @@
|
|||
}
|
||||
},
|
||||
"validator": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-11.0.0.tgz",
|
||||
"integrity": "sha512-+wnGLYqaKV2++nUv60uGzUJyJQwYVOin6pn1tgEiFCeCQO60yeu3Og9/yPccbBX574kxIcEJicogkzx6s6eyag=="
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz",
|
||||
"integrity": "sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
"coverage": "cat coverage/lcov.info | codacy-coverage --token 8801f827fe1144ffa85cd7da94f2bbf7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sendgrid/mail": "^6.4.0",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"debug": "^4.1.1",
|
||||
"dotenv": "^8.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-validator": "^6.0.1",
|
||||
"express-validator": "^6.1.1",
|
||||
"morgan": "^1.9.1",
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
|
@ -28,15 +30,15 @@
|
|||
"@types/express": "^4.17.0",
|
||||
"@types/jest": "^24.0.15",
|
||||
"@types/morgan": "^1.7.35",
|
||||
"@types/node": "^11.13.15",
|
||||
"@types/node": "^12.0.12",
|
||||
"@types/request": "^2.48.1",
|
||||
"@types/supertest": "^2.0.7",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"jest": "^24.8.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"supertest": "^4.0.2",
|
||||
"ts-jest": "^24.0.2",
|
||||
"ts-node": "^8.3.0",
|
||||
"typescript": "3.4.5"
|
||||
"typescript": "3.5.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
36
server/src/routes/ReportRouter.ts
Normal file
36
server/src/routes/ReportRouter.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Router, Request, Response } from 'express'
|
||||
import SendgridMail from '@sendgrid/mail'
|
||||
import 'dotenv/config'
|
||||
|
||||
SendgridMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
|
||||
export class ReportRouter {
|
||||
public router: Router
|
||||
|
||||
public constructor() {
|
||||
this.router = Router()
|
||||
}
|
||||
|
||||
public async sendMessage(req: Request, res: Response) {
|
||||
if (!req.body.msg) {
|
||||
return res.send({ status: 'error', message: 'missing message' })
|
||||
}
|
||||
|
||||
try {
|
||||
await SendgridMail.send(req.body.msg)
|
||||
return res.send({ status: 'success' })
|
||||
} catch (error) {
|
||||
console.error(`${error.code} - ${error.message}`) // eslint-disable-line
|
||||
res.send(`${error.code} - ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
public init() {
|
||||
this.router.post('/', this.sendMessage)
|
||||
}
|
||||
}
|
||||
|
||||
const reportRoutes = new ReportRouter()
|
||||
reportRoutes.init()
|
||||
|
||||
export default reportRoutes.router
|
|
@ -7,6 +7,7 @@ import pkg from '../../package.json'
|
|||
|
||||
// routes
|
||||
import UrlCheckRouter from './routes/UrlCheckRouter'
|
||||
import ReportRouter from './routes/ReportRouter'
|
||||
|
||||
// config
|
||||
import config from './config/config'
|
||||
|
@ -62,6 +63,7 @@ app.get('/', (req, res) => {
|
|||
)
|
||||
})
|
||||
app.use('/api/v1/urlcheck', UrlCheckRouter)
|
||||
app.use('/api/v1/report', ReportRouter)
|
||||
|
||||
/// catch 404
|
||||
app.use((req, res) => {
|
||||
|
|
|
@ -25,6 +25,14 @@ describe('POST /api/v1/urlcheck', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('POST /api/v1/report', () => {
|
||||
it('responds with error message when message is missing', async () => {
|
||||
const response = await request(server).post('/api/v1/report')
|
||||
const text = await JSON.parse(response.text)
|
||||
expect(text.message).toBe('missing message')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Errors', () => {
|
||||
it('responds with 404 on unknown path', async () => {
|
||||
const response = await request(server).post('/whatever')
|
||||
|
|
Loading…
Reference in New Issue
Block a user