mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 15:25:57 +00:00
fix: Re-enable webfinger feature
Ok, so here is the plan. Let's give both our cucumber features and your cypress tests a prominent place to live. That would be the root level folder of our application. Second, let's revive formerly dead code step by step. Ie. move code from the former location `backend/features/` to `features/` when it is ready. All edge cases should be tested with unit tests in `backend/`, see my `webfinger.spec.js` as an example.
This commit is contained in:
parent
35c3219460
commit
7c6d5b5129
@ -8,13 +8,13 @@ addons:
|
||||
- docker
|
||||
- chromium
|
||||
|
||||
before_install:
|
||||
install:
|
||||
- yarn global add wait-on
|
||||
# Install Codecov
|
||||
- yarn install
|
||||
- cp cypress.env.template.json cypress.env.json
|
||||
|
||||
install:
|
||||
before_script:
|
||||
- docker-compose -f docker-compose.yml build --parallel
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml build # just tagging, just be quite fast
|
||||
- docker-compose -f docker-compose.yml -f docker-compose.build-and-test.yml up -d
|
||||
@ -30,10 +30,6 @@ script:
|
||||
- docker-compose exec backend yarn run test --ci --verbose=false --coverage
|
||||
- docker-compose exec backend yarn run db:seed
|
||||
- docker-compose exec backend yarn run db:reset
|
||||
# ActivityPub cucumber testing temporarily disabled because it's too buggy
|
||||
# - docker-compose exec backend yarn run test:cucumber --tags "not @wip"
|
||||
# - docker-compose exec backend yarn run db:reset
|
||||
# - docker-compose exec backend yarn run db:seed
|
||||
# Frontend
|
||||
- docker-compose exec webapp yarn run lint
|
||||
- docker-compose exec webapp yarn run test --ci --verbose=false --coverage
|
||||
@ -42,6 +38,7 @@ script:
|
||||
- docker-compose -f docker-compose.yml up -d
|
||||
- wait-on http://localhost:7474
|
||||
- yarn run cypress:run --record
|
||||
- yarn run cucumber
|
||||
# Coverage
|
||||
- yarn run codecov
|
||||
|
||||
|
||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"node": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@ -10,8 +10,6 @@
|
||||
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/index.js -e js,gql",
|
||||
"lint": "eslint src --config .eslintrc.js",
|
||||
"test": "jest --forceExit --detectOpenHandles --runInBand",
|
||||
"test:cucumber:cmd": "wait-on tcp:4001 tcp:4123 && cucumber-js --require-module @babel/register --exit test/",
|
||||
"test:cucumber": " cross-env CLIENT_URI=http://localhost:4123 run-p --race test:before:* 'test:cucumber:cmd {@}' --",
|
||||
"db:reset": "babel-node src/seed/reset-db.js",
|
||||
"db:seed": "babel-node src/seed/seed-db.js"
|
||||
},
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
import user from './user'
|
||||
import inbox from './inbox'
|
||||
import webFinger from './webFinger'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import verify from './verify'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use('/.well-known/webFinger', cors(), express.urlencoded({ extended: true }), webFinger)
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({ type: ['application/activity+json', 'application/ld+json', 'application/json'] }),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
|
||||
export default router
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use(
|
||||
'/activitypub/users',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
user,
|
||||
)
|
||||
router.use(
|
||||
'/activitypub/inbox',
|
||||
cors(),
|
||||
express.json({
|
||||
type: ['application/activity+json', 'application/ld+json', 'application/json'],
|
||||
}),
|
||||
express.urlencoded({ extended: true }),
|
||||
verify,
|
||||
inbox,
|
||||
)
|
||||
return router
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import express from 'express'
|
||||
import { createWebFinger } from '../utils/actor'
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', async function(req, res) {
|
||||
const resource = req.query.resource
|
||||
if (!resource || !resource.includes('acct:')) {
|
||||
return res
|
||||
.status(400)
|
||||
.send(
|
||||
'Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.',
|
||||
)
|
||||
} else {
|
||||
const nameAndDomain = resource.replace('acct:', '')
|
||||
const name = nameAndDomain.split('@')[0]
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await req.app.get('ap').dataSource.client.query({
|
||||
query: gql`
|
||||
query {
|
||||
User(slug: "${name}") {
|
||||
slug
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error })
|
||||
}
|
||||
|
||||
if (result.data && result.data.User.length > 0) {
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} else {
|
||||
return res.status(404).json({ error: `No record found for ${nameAndDomain}.` })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
59
backend/src/activitypub/routes/webfinger.js
Normal file
59
backend/src/activitypub/routes/webfinger.js
Normal file
@ -0,0 +1,59 @@
|
||||
import express from 'express'
|
||||
import CONFIG from '../../config/'
|
||||
import cors from 'cors'
|
||||
|
||||
const debug = require('debug')('ea:webfinger')
|
||||
const regex = /acct:([a-z0-9_-]*)@([a-z0-9_-]*)/
|
||||
|
||||
const createWebFinger = name => {
|
||||
const { host } = new URL(CONFIG.CLIENT_URI)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${CONFIG.CLIENT_URI}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export async function handler(req, res) {
|
||||
const { resource = '' } = req.query
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [_, name, domain] = resource.match(regex) || []
|
||||
if (!(name && domain))
|
||||
return res.status(400).json({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
|
||||
const session = req.app.get('driver').session()
|
||||
try {
|
||||
const [slug] = await session.readTransaction(async t => {
|
||||
const result = await t.run('MATCH (u:User {slug: $slug}) RETURN u.slug AS slug', {
|
||||
slug: name,
|
||||
})
|
||||
return result.records.map(record => record.get('slug'))
|
||||
})
|
||||
if (!slug)
|
||||
return res.status(404).json({
|
||||
error: `No record found for "${name}@${domain}".`,
|
||||
})
|
||||
const webFinger = createWebFinger(name)
|
||||
return res.contentType('application/jrd+json').json(webFinger)
|
||||
} catch (error) {
|
||||
debug(error)
|
||||
return res.status(500).json({
|
||||
error: 'Something went terribly wrong. Please contact support@human-connection.org',
|
||||
})
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default function() {
|
||||
const router = express.Router()
|
||||
router.use('/webfinger', cors(), express.urlencoded({ extended: true }), handler)
|
||||
return router
|
||||
}
|
||||
117
backend/src/activitypub/routes/webfinger.spec.js
Normal file
117
backend/src/activitypub/routes/webfinger.spec.js
Normal file
@ -0,0 +1,117 @@
|
||||
import { handler } from './webfinger'
|
||||
import Factory from '../../seed/factories'
|
||||
import { getDriver } from '../../bootstrap/neo4j'
|
||||
|
||||
let resource
|
||||
let res
|
||||
let json
|
||||
let status
|
||||
let contentType
|
||||
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
|
||||
const request = () => {
|
||||
json = jest.fn()
|
||||
status = jest.fn(() => ({ json }))
|
||||
contentType = jest.fn(() => ({ json }))
|
||||
res = { status, contentType }
|
||||
const req = {
|
||||
app: {
|
||||
get: key => {
|
||||
return {
|
||||
driver,
|
||||
}[key]
|
||||
},
|
||||
},
|
||||
query: {
|
||||
resource,
|
||||
},
|
||||
}
|
||||
return handler(req, res)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('webfinger', () => {
|
||||
describe('no ressource', () => {
|
||||
beforeEach(() => {
|
||||
resource = undefined
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('?ressource query param', () => {
|
||||
describe('is missing acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'some-user@domain'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('has no domain', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@'
|
||||
})
|
||||
|
||||
it('sends HTTP 400', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(400)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'Query parameter "?resource=acct:<USER>@<DOMAIN>" is missing.',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with acct:', () => {
|
||||
beforeEach(() => {
|
||||
resource = 'acct:some-user@domain'
|
||||
})
|
||||
|
||||
it('returns empty json', async () => {
|
||||
await request()
|
||||
expect(status).toHaveBeenCalledWith(404)
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
error: 'No record found for "some-user@domain".',
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a user for acct', () => {
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', { slug: 'some-user' })
|
||||
})
|
||||
|
||||
it('returns user object', async () => {
|
||||
await request()
|
||||
expect(contentType).toHaveBeenCalledWith('application/jrd+json')
|
||||
expect(json).toHaveBeenCalledWith({
|
||||
links: [
|
||||
{
|
||||
href: 'http://localhost:3000/activitypub/users/some-user',
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
},
|
||||
],
|
||||
subject: 'acct:some-user@localhost:3000',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -22,17 +22,3 @@ export function createActor(name, pubkey) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebFinger(name) {
|
||||
const { host } = new URL(activityPub.endpoint)
|
||||
return {
|
||||
subject: `acct:${name}@${host}`,
|
||||
links: [
|
||||
{
|
||||
rel: 'self',
|
||||
type: 'application/activity+json',
|
||||
href: `${activityPub.endpoint}/activitypub/users/${name}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
|
||||
dotenv.config()
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') })
|
||||
|
||||
const {
|
||||
MAPBOX_TOKEN,
|
||||
|
||||
@ -6,6 +6,7 @@ import middleware from './middleware'
|
||||
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
|
||||
import decode from './jwt/decode'
|
||||
import schema from './schema'
|
||||
import webfinger from './activitypub/routes/webfinger'
|
||||
|
||||
// check required configs and throw error
|
||||
// TODO check this directly in config file - currently not possible due to testsetup
|
||||
@ -41,7 +42,10 @@ const createServer = options => {
|
||||
const server = new ApolloServer(Object.assign({}, defaults, options))
|
||||
|
||||
const app = express()
|
||||
|
||||
app.set('driver', driver)
|
||||
app.use(helmet())
|
||||
app.use('/.well-known/', webfinger())
|
||||
app.use(express.static('public'))
|
||||
server.applyMiddleware({ app, path: '/' })
|
||||
|
||||
|
||||
@ -9,32 +9,6 @@ Feature: Webfinger discovery
|
||||
| Slug |
|
||||
| peter-lustiger |
|
||||
|
||||
Scenario: Search
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
||||
Then I receive the following json:
|
||||
"""
|
||||
{
|
||||
"subject": "acct:peter-lustiger@localhost:4123",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "http://localhost:4123/activitypub/users/peter-lustiger"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
And I expect the Content-Type to be "application/jrd+json; charset=utf-8"
|
||||
|
||||
Scenario: User does not exist
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
||||
Then I receive the following json:
|
||||
"""
|
||||
{
|
||||
"error": "No record found for nonexisting@localhost."
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: Receiving an actor object
|
||||
When I send a GET request to "/activitypub/users/peter-lustiger"
|
||||
Then I receive the following json:
|
||||
|
||||
@ -9,7 +9,7 @@ open your minikube dashboard:
|
||||
$ minikube dashboard
|
||||
```
|
||||
|
||||
This will give you an overview. Some of the steps below need some timing to make ressources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
||||
This will give you an overview. Some of the steps below need some timing to make resources available to other dependent deployments. Keeping an eye on the dashboard is a great way to check that.
|
||||
|
||||
Follow the installation instruction for [Human Connection](../human-connection/README.md).
|
||||
If all the pods and services have settled and everything looks green in your
|
||||
|
||||
45
features/support/steps.js
Normal file
45
features/support/steps.js
Normal file
@ -0,0 +1,45 @@
|
||||
// features/support/steps.js
|
||||
import { Given, When, Then, After, AfterAll } from 'cucumber'
|
||||
import Factory from '../../backend/src/seed/factories'
|
||||
import dotenv from 'dotenv'
|
||||
import expect from 'expect'
|
||||
|
||||
const debug = require('debug')('ea:test:steps')
|
||||
const factory = Factory()
|
||||
|
||||
|
||||
After(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
Given('our CLIENT_URI is {string}', function (string) {
|
||||
expect(process.env.CLIENT_URI).toEqual(string)
|
||||
});
|
||||
|
||||
Given('we have the following users in our database:', function (dataTable) {
|
||||
return Promise.all(dataTable.hashes().map(({ slug, name }) => {
|
||||
return factory.create('User', {
|
||||
name,
|
||||
slug,
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
When('I send a GET request to {string}', async function (pathname) {
|
||||
const response = await this.get(pathname)
|
||||
this.lastContentType = response.lastContentType
|
||||
|
||||
this.lastResponses.push(response.lastResponse)
|
||||
this.statusCode = response.statusCode
|
||||
})
|
||||
|
||||
Then('the server responds with a HTTP Status {int} and the following json:', function (statusCode, docString) {
|
||||
expect(this.statusCode).toEqual(statusCode)
|
||||
const [ lastResponse ] = this.lastResponses
|
||||
expect(JSON.parse(lastResponse)).toMatchObject(JSON.parse(docString))
|
||||
})
|
||||
|
||||
Then('the Content-Type is {string}', function (contentType) {
|
||||
expect(this.lastContentType).toEqual(contentType)
|
||||
})
|
||||
|
||||
36
features/webfinger.feature
Normal file
36
features/webfinger.feature
Normal file
@ -0,0 +1,36 @@
|
||||
Feature: Webfinger discovery
|
||||
From an external server, e.g. Mastodon
|
||||
I want to search for an actor alias
|
||||
In order to follow the actor
|
||||
|
||||
Background:
|
||||
Given our CLIENT_URI is "http://localhost:3000"
|
||||
And we have the following users in our database:
|
||||
| name | slug |
|
||||
| Peter Lustiger | peter-lustiger |
|
||||
|
||||
Scenario: Search a user
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:peter-lustiger@localhost"
|
||||
Then the server responds with a HTTP Status 200 and the following json:
|
||||
"""
|
||||
{
|
||||
"subject": "acct:peter-lustiger@localhost:3000",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": "http://localhost:3000/activitypub/users/peter-lustiger"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
And the Content-Type is "application/jrd+json; charset=utf-8"
|
||||
|
||||
Scenario: Search without result
|
||||
When I send a GET request to "/.well-known/webfinger?resource=acct:nonexisting@localhost"
|
||||
Then the server responds with a HTTP Status 404 and the following json:
|
||||
"""
|
||||
{
|
||||
"error": "No record found for \"nonexisting@localhost\"."
|
||||
}
|
||||
"""
|
||||
38
features/world.js
Normal file
38
features/world.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { setWorldConstructor } from 'cucumber'
|
||||
import request from 'request'
|
||||
|
||||
class CustomWorld {
|
||||
constructor () {
|
||||
// webFinger.feature
|
||||
this.lastResponses = []
|
||||
this.lastContentType = null
|
||||
this.lastInboxUrl = null
|
||||
this.lastActivity = null
|
||||
// object-article.feature
|
||||
this.statusCode = null
|
||||
}
|
||||
get (pathname) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request(`http://localhost:4000/${this.replaceSlashes(pathname)}`, {
|
||||
headers: {
|
||||
'Accept': 'application/activity+json'
|
||||
}}, (error, response, body) => {
|
||||
if (!error) {
|
||||
resolve({
|
||||
lastResponse: body,
|
||||
lastContentType: response.headers['content-type'],
|
||||
statusCode: response.statusCode
|
||||
})
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
replaceSlashes (pathname) {
|
||||
return pathname.replace(/^\/+/, '')
|
||||
}
|
||||
}
|
||||
|
||||
setWorldConstructor(CustomWorld)
|
||||
11
package.json
11
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nitro-cypress",
|
||||
"name": "human-connection",
|
||||
"version": "0.1.11",
|
||||
"description": "Fullstack tests with cypress for Human Connection",
|
||||
"description": "Fullstack and API tests with cypress and cucumber for Human Connection",
|
||||
"author": "Human Connection gGmbh",
|
||||
"license": "MIT",
|
||||
"cypress-cucumber-preprocessor": {
|
||||
@ -16,19 +16,26 @@
|
||||
"cypress:setup": "run-p cypress:backend cypress:webapp",
|
||||
"cypress:run": "cross-env cypress run --browser chromium",
|
||||
"cypress:open": "cross-env cypress open --browser chromium",
|
||||
"cucumber:setup": "cd backend && yarn run dev",
|
||||
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
|
||||
"version": "auto-changelog -p"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"auto-changelog": "^1.16.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"codecov": "^3.6.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"cucumber": "^6.0.5",
|
||||
"cypress": "^3.7.0",
|
||||
"cypress-cucumber-preprocessor": "^1.17.0",
|
||||
"cypress-file-upload": "^3.5.0",
|
||||
"cypress-plugin-retries": "^1.5.0",
|
||||
"date-fns": "^2.8.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"expect": "^24.9.0",
|
||||
"faker": "Marak/faker.js#master",
|
||||
"graphql-request": "^1.8.2",
|
||||
"neo4j-driver": "^1.7.6",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user