Skip to content

Commit

Permalink
Implement endpoint to get info about subscriptions of current user (#4)
Browse files Browse the repository at this point in the history
Also write tests for it
  • Loading branch information
mrkvon authored Oct 11, 2023
1 parent f674352 commit 094d40b
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 35 deletions.
4 changes: 4 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# TODO

- re-subscribe when subscription expires
- make nice email messages
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
finishIntegration,
initializeIntegration,
} from './controllers/integration'
import { getStatus } from './controllers/status'
import { webhookReceiver } from './controllers/webhookReceiver'
import { solidAuth } from './middlewares/solidAuth'

Expand All @@ -22,6 +23,7 @@ router
.post('/inbox', solidAuth, initializeIntegration)
.get('/verify-email', checkVerificationLink, finishIntegration)
.post('/webhook-receiver', webhookReceiver)
.get('/status', solidAuth, getStatus)

app
.use(helmet())
Expand Down
27 changes: 27 additions & 0 deletions src/controllers/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Middleware } from 'koa'
import { EmailVerification, Integration } from '../config/sequelize'

export const getStatus: Middleware = async ctx => {
const webId = ctx.state.user

const verified = await Integration.findAll({ where: { webId } })

const unverified = await EmailVerification.findAll({ where: { webId } })

ctx.response.body = {
actor: webId,
integrations: [
...verified.map(s => ({
object: s.inbox,
target: s.email,
verified: true,
})),
...unverified.map(s => ({
object: s.inbox,
target: s.email,
verified: false,
})),
],
}
ctx.response.status = 200
}
57 changes: 57 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
createDpopHeader,
generateDpopKeyPair,
} from '@inrupt/solid-client-authn-core'
import { expect } from 'chai'
import * as uuid from 'uuid'

type Credentials = { email: string; password: string }

Expand Down Expand Up @@ -55,3 +57,58 @@ export const getAuthenticatedFetch = async ({

return authFetch
}

export const createAccount = async ({
username,
password,
email,
solidServer,
}: {
username: string
password?: string
email?: string
solidServer: string
}) => {
password ??= 'correcthorsebatterystaple'
email ??= username + '@example.org'
const config = {
idp: solidServer + '/',
podUrl: `${solidServer}/${username}/`,
webId: `${solidServer}/${username}/profile/card#me`,
username,
password,
email,
}
const registerEndpoint = solidServer + '/idp/register/'
const response = await fetch(registerEndpoint, {
method: 'post',
body: JSON.stringify({
email,
password,
confirmPassword: password,
createWebId: true,
register: true,
createPod: true,
rootPod: false,
podName: username,
}),
headers: { 'content-type': 'application/json' },
})

expect(response.ok).to.be.true

return config
}

export const createRandomAccount = ({
solidServer,
}: {
solidServer: string
}) => {
return createAccount({
username: uuid.v4(),
password: uuid.v4(),
email: uuid.v4() + '@example.com',
solidServer,
})
}
6 changes: 3 additions & 3 deletions src/test/integration-finish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Mail from 'nodemailer/lib/mailer'
import { SinonSandbox, SinonSpy, createSandbox } from 'sinon'
import { baseUrl } from '../config'
import * as mailerService from '../services/mailerService'
import { authenticatedFetch } from './testSetup.spec'
import { authenticatedFetch, person } from './testSetup.spec'

describe('email verification via /verify-email?id=webId&token=base64Token', () => {
let sendMailSpy: SinonSpy<[options: Mail.Options], Promise<void>>
Expand Down Expand Up @@ -34,8 +34,8 @@ describe('email verification via /verify-email?id=webId&token=base64Token', () =
'@context': 'https://www.w3.org/ns/activitystreams',
'@id': '',
'@type': 'Add',
actor: 'http://localhost:3456/person/profile/card#me',
object: 'http://localhost:3456/person/profile/card',
actor: person.webId,
object: person.podUrl + 'profile/card',
target: '[email protected]',
}),
})
Expand Down
24 changes: 8 additions & 16 deletions src/test/integration-start.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Mail from 'nodemailer/lib/mailer'
import { SinonSandbox, SinonSpy, createSandbox } from 'sinon'
import { baseUrl } from '../config'
import * as mailerService from '../services/mailerService'
import { authenticatedFetch } from './testSetup.spec'
import { authenticatedFetch, person } from './testSetup.spec'

describe('Mailer integration via /inbox', () => {
let sendMailSpy: SinonSpy<[options: Mail.Options], Promise<void>>
Expand All @@ -30,8 +30,8 @@ describe('Mailer integration via /inbox', () => {
'@context': 'https://www.w3.org/ns/activitystreams',
'@id': '',
'@type': 'Add',
actor: 'http://localhost:3456/person/profile/card#me',
object: 'http://localhost:3456/person/inbox/',
actor: person.webId,
object: person.podUrl + 'inbox/',
target: '[email protected]',
}),
})
Expand All @@ -43,18 +43,10 @@ describe('Mailer integration via /inbox', () => {
)
expect(sendMailSpy.firstCall.firstArg)
.to.haveOwnProperty('text')
.include(
`verify-email?id=${encodeURIComponent(
'http://localhost:3456/person/profile/card#me',
)}&token=`,
)
.include(`verify-email?id=${encodeURIComponent(person.webId)}&token=`)
expect(sendMailSpy.firstCall.firstArg)
.to.haveOwnProperty('html')
.include(
`verify-email?id=${encodeURIComponent(
'http://localhost:3456/person/profile/card#me',
)}&token=`,
)
.include(`verify-email?id=${encodeURIComponent(person.webId)}&token=`)
expect(response.status).to.equal(200)
})

Expand All @@ -67,8 +59,8 @@ describe('Mailer integration via /inbox', () => {
'@context': 'https://www.w3.org/ns/activitystreams',
'@id': '',
'@type': 'Add',
actor: 'http://localhost:3456/person/profile/card#me',
object: 'http://localhost:3456/person/inbox/',
actor: person.webId,
object: person.podUrl + 'inbox/',
target: '[email protected]',
}),
})
Expand All @@ -87,7 +79,7 @@ describe('Mailer integration via /inbox', () => {
'@id': '',
'@type': 'Add',
actor: 'http://localhost:3456/person2/profile/card#me',
object: 'http://localhost:3456/person/inbox/',
object: person.podUrl + 'inbox/',
target: '[email protected]',
}),
})
Expand Down
18 changes: 11 additions & 7 deletions src/test/notification.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { baseUrl } from '../config'
import { getAuthenticatedFetch } from '../helpers'
import * as mailerService from '../services/mailerService'
import { addRead, setupInbox } from '../setup'
import { authenticatedFetch } from './testSetup.spec'
import { authenticatedFetch, person } from './testSetup.spec'

describe('received notification via /webhook-receiver', () => {
let sendMailSpy: SinonSpy<[options: Mail.Options], Promise<void>>
Expand All @@ -20,15 +20,19 @@ describe('received notification via /webhook-receiver', () => {
sendMailSpy = sandbox.spy(mailerService, 'sendMail')
})

afterEach(() => {
sandbox.restore()
})

beforeEach(async () => {
await setupInbox({
webId: 'http://localhost:3456/person/profile/card#me',
inbox: 'http://localhost:3456/person/inbox/',
webId: person.webId,
inbox: person.podUrl + 'inbox/',
authenticatedFetch,
})

await addRead({
resource: 'http://localhost:3456/person/inbox/',
resource: person.podUrl + 'inbox/',
agent: 'http://localhost:3456/bot/profile/card#me',
authenticatedFetch,
})
Expand All @@ -47,8 +51,8 @@ describe('received notification via /webhook-receiver', () => {
'@context': 'https://www.w3.org/ns/activitystreams',
'@id': '',
'@type': 'Add',
actor: 'http://localhost:3456/person/profile/card#me',
object: 'http://localhost:3456/person/inbox/',
actor: person.webId,
object: person.podUrl + 'inbox/',
target: '[email protected]',
}),
})
Expand All @@ -74,7 +78,7 @@ describe('received notification via /webhook-receiver', () => {
solidServer: 'http://localhost:3456',
})
const addToInboxResponse = await authenticatedPerson2Fetch(
'http://localhost:3456/person/inbox/',
person.podUrl + 'inbox/',
{
method: 'POST',
headers: {
Expand Down
110 changes: 110 additions & 0 deletions src/test/status.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { expect } from 'chai'
import * as cheerio from 'cheerio'
import { describe } from 'mocha'
import Mail from 'nodemailer/lib/mailer'
import { SinonSandbox, SinonSpy, createSandbox } from 'sinon'
import { baseUrl } from '../config'
import * as mailerService from '../services/mailerService'
import { addRead, setupInbox } from '../setup'
import { authenticatedFetch, person } from './testSetup.spec'

describe('get info about integrations of current person with GET /status', () => {
let sendMailSpy: SinonSpy<[options: Mail.Options], Promise<void>>
let verificationLink: string
let sandbox: SinonSandbox

beforeEach(() => {
sandbox = createSandbox()
sendMailSpy = sandbox.spy(mailerService, 'sendMail')
})

afterEach(() => {
sandbox.restore()
})

beforeEach(async () => {
await setupInbox({
webId: person.webId,
inbox: `${person.podUrl}inbox/`,
authenticatedFetch,
})

await addRead({
resource: `${person.podUrl}inbox/`,
agent: 'http://localhost:3456/bot/profile/card#me',
authenticatedFetch,
})
})

beforeEach(async function () {
this.timeout(10000)
// initialize the integration
const initResponse = await authenticatedFetch(`${baseUrl}/inbox`, {
method: 'post',
headers: {
'content-type':
'application/ld+json;profile="https://www.w3.org/ns/activitystreams"',
},
body: JSON.stringify({
'@context': 'https://www.w3.org/ns/activitystreams',
'@id': '',
'@type': 'Add',
actor: person.webId,
object: `${person.podUrl}inbox/`,
target: '[email protected]',
}),
})

expect(initResponse.status).to.equal(200)
// email was sent
const emailMessage = sendMailSpy.firstCall.firstArg.html
const $ = cheerio.load(emailMessage)
verificationLink = $('a').first().attr('href') as string
expect(verificationLink).to.not.be.null
})

it('[not authenticated] should fail with 401', async () => {
const response = await fetch(`${baseUrl}/status`)
expect(response.status).to.equal(401)
})

it('should show list of resources (inboxes) current user is observing', async () => {
// finish the integration
const finishResponse = await fetch(verificationLink)
expect(finishResponse.status).to.equal(200)

const response = await authenticatedFetch(`${baseUrl}/status`)
expect(response.status).to.equal(200)

const body = await response.json()

expect(body).to.deep.equal({
actor: person.webId,
integrations: [
{
object: `${person.podUrl}inbox/`,
target: '[email protected]',
verified: true,
},
],
})
})

it('should show unverified integrations', async () => {
const response = await authenticatedFetch(`${baseUrl}/status`)
expect(response.status).to.equal(200)

const body = await response.json()

expect(body).to.deep.equal({
actor: person.webId,
integrations: [
{
object: `${person.podUrl}inbox/`,
target: '[email protected]',
verified: false,
},
],
})
})
})
Loading

0 comments on commit 094d40b

Please sign in to comment.