diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..39b5929 --- /dev/null +++ b/TODO.md @@ -0,0 +1,4 @@ +# TODO + +- re-subscribe when subscription expires +- make nice email messages diff --git a/src/app.ts b/src/app.ts index 4338bd0..ca15d0d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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' @@ -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()) diff --git a/src/controllers/status.ts b/src/controllers/status.ts new file mode 100644 index 0000000..364d3d9 --- /dev/null +++ b/src/controllers/status.ts @@ -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 +} diff --git a/src/helpers.ts b/src/helpers.ts index 9b6d11b..1c36e4b 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -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 } @@ -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, + }) +} diff --git a/src/test/integration-finish.spec.ts b/src/test/integration-finish.spec.ts index dac4412..ea9732f 100644 --- a/src/test/integration-finish.spec.ts +++ b/src/test/integration-finish.spec.ts @@ -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> @@ -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@example.com', }), }) diff --git a/src/test/integration-start.spec.ts b/src/test/integration-start.spec.ts index 01e4e55..4b9314c 100644 --- a/src/test/integration-start.spec.ts +++ b/src/test/integration-start.spec.ts @@ -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> @@ -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@example.com', }), }) @@ -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) }) @@ -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@example.com', }), }) @@ -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: 'email2@example.com', }), }) diff --git a/src/test/notification.spec.ts b/src/test/notification.spec.ts index b9d1961..75e6050 100644 --- a/src/test/notification.spec.ts +++ b/src/test/notification.spec.ts @@ -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> @@ -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, }) @@ -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@example.com', }), }) @@ -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: { diff --git a/src/test/status.spec.ts b/src/test/status.spec.ts new file mode 100644 index 0000000..d2f10a8 --- /dev/null +++ b/src/test/status.spec.ts @@ -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> + 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@example.com', + }), + }) + + 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@example.com', + 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@example.com', + verified: false, + }, + ], + }) + }) +}) diff --git a/src/test/testSetup.spec.ts b/src/test/testSetup.spec.ts index 1117328..a2750cc 100644 --- a/src/test/testSetup.spec.ts +++ b/src/test/testSetup.spec.ts @@ -3,11 +3,19 @@ import { IncomingMessage, Server, ServerResponse } from 'http' import MailDev from 'maildev' import app from '../app' import { port } from '../config' -import { EmailVerification } from '../config/sequelize' -import { getAuthenticatedFetch } from '../helpers' +import { EmailVerification, Integration } from '../config/sequelize' +import { createRandomAccount, getAuthenticatedFetch } from '../helpers' let server: Server let authenticatedFetch: typeof fetch +let person: { + idp: string + podUrl: string + webId: string + username: string + password: string + email: string +} let cssServer: css.App before(async function () { @@ -37,13 +45,8 @@ before(async function () { // eslint-disable-next-line no-console console.log('CSS server started in', (Date.now() - start) / 1000, 'seconds') - - authenticatedFetch = await getAuthenticatedFetch({ - email: 'person@example', - password: 'password', - solidServer: 'http://localhost:3456', - }) }) + after(async () => { await cssServer.stop() }) @@ -71,6 +74,19 @@ after(done => { // clear the database before each test beforeEach(async () => { await EmailVerification.destroy({ truncate: true }) + await Integration.destroy({ truncate: true }) +}) + +/** + * Before each test, create a new account and authenticate to it + */ +beforeEach(async () => { + person = await createRandomAccount({ solidServer: 'http://localhost:3456' }) + authenticatedFetch = await getAuthenticatedFetch({ + email: person.email, + password: person.password, + solidServer: 'http://localhost:3456', + }) }) -export { authenticatedFetch, cssServer, server } +export { authenticatedFetch, cssServer, person, server }