{origin}
' }) + const sub = makeSubscription() + const notif = prepareSubscriptionNotification(event, sub, {}, 'fr', 'n1') + expect(notif.htmlBody).toBe('https://example.com
') + }) + + test('inherits outputs from subscription when not on event', () => { + const event = makeEvent() + const sub = makeSubscription({ outputs: ['email'] }) + const notif = prepareSubscriptionNotification(event, sub, {}, 'fr', 'n1') + expect(notif.outputs).toEqual(['email']) + }) + + test('removes resource, originator, urlParams from notification', () => { + const event = makeEvent({ + resource: { type: 'dataset', id: 'd1' }, + originator: { user: { id: 'u1' } }, + urlParams: { x: '1' } + }) + const sub = makeSubscription() + const notif = prepareSubscriptionNotification(event, sub, {}, 'fr', 'n1') + expect((notif as any).resource).toBeUndefined() + expect((notif as any).originator).toBeUndefined() + expect((notif as any).urlParams).toBeUndefined() + }) + + test('localizes event with subscription locale', () => { + const event = makeEvent({ title: { fr: 'Titre', en: 'Title' } }) + const sub = makeSubscription({ locale: 'en' }) + const notif = prepareSubscriptionNotification(event, sub, {}, 'fr', 'n1') + expect(notif.title).toBe('Title') + }) +}) diff --git a/tests/push.unit.spec.ts b/tests/push.unit.spec.ts new file mode 100644 index 0000000..c4b32ba --- /dev/null +++ b/tests/push.unit.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' +import { equalDeviceRegistrations } from '../api/src/push/operations.ts' + +test.describe('equalDeviceRegistrations', () => { + test('returns false for null first arg', () => { + expect(equalDeviceRegistrations(null, 'abc')).toBe(false) + }) + + test('returns false for null second arg', () => { + expect(equalDeviceRegistrations('abc', null)).toBe(false) + }) + + test('returns false for both null', () => { + expect(equalDeviceRegistrations(null, null)).toBe(false) + }) + + test('returns true for equal strings', () => { + expect(equalDeviceRegistrations('abc', 'abc')).toBe(true) + }) + + test('returns false for different strings', () => { + expect(equalDeviceRegistrations('abc', 'def')).toBe(false) + }) + + test('returns true for objects with same endpoint', () => { + expect(equalDeviceRegistrations( + { endpoint: 'https://push.example.com/1' }, + { endpoint: 'https://push.example.com/1' } + )).toBe(true) + }) + + test('returns false for objects with different endpoints', () => { + expect(equalDeviceRegistrations( + { endpoint: 'https://push.example.com/1' }, + { endpoint: 'https://push.example.com/2' } + )).toBe(false) + }) + + test('returns false for mixed string and object', () => { + expect(equalDeviceRegistrations('abc', { endpoint: 'abc' })).toBe(false) + }) +}) diff --git a/tests/seed.spec.ts b/tests/seed.spec.ts new file mode 100644 index 0000000..4e835b9 --- /dev/null +++ b/tests/seed.spec.ts @@ -0,0 +1,7 @@ +import { test } from '@playwright/test' + +test.describe('Test group', () => { + test('seed', async ({ page }) => { + // generate code here. + }) +}) diff --git a/tests/shared.unit.spec.ts b/tests/shared.unit.spec.ts new file mode 100644 index 0000000..c20afa4 --- /dev/null +++ b/tests/shared.unit.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '@playwright/test' +import { backoffMinutes } from '../api/src/shared/operations.ts' + +test.describe('backoffMinutes', () => { + test('returns 1 for 1 error', () => { + expect(backoffMinutes(1)).toBe(1) + }) + + test('returns 6 for 2 errors', () => { + // Math.ceil(2^2.5) = Math.ceil(5.656) = 6 + expect(backoffMinutes(2)).toBe(6) + }) + + test('returns 16 for 3 errors', () => { + // Math.ceil(3^2.5) = Math.ceil(15.588) = 16 + expect(backoffMinutes(3)).toBe(16) + }) + + test('returns 32 for 4 errors', () => { + // Math.ceil(4^2.5) = Math.ceil(32) = 32 + expect(backoffMinutes(4)).toBe(32) + }) + + test('grows super-linearly', () => { + const b3 = backoffMinutes(3) + const b5 = backoffMinutes(5) + const b9 = backoffMinutes(9) + expect(b5).toBeGreaterThan(b3) + expect(b9).toBeGreaterThan(b5) + // ratio should increase + expect(b9 / b5).toBeGreaterThan(b5 / b3) + }) +}) diff --git a/tests/state-setup.ts b/tests/state-setup.ts new file mode 100644 index 0000000..7481f08 --- /dev/null +++ b/tests/state-setup.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict' +import { spawn } from 'child_process' +import { axiosBuilder } from '@data-fair/lib-node/axios.js' +import { test as setup } from '@playwright/test' + +const anonymousAx = axiosBuilder() + +setup('Stateful tests setup', async () => { + await assert.doesNotReject(anonymousAx.get(`http://localhost:${process.env.DEV_API_PORT}/api/ping`), + `Dev web server seems to be unavailable. +If you are agent do not try to start it. Instead check for a startup failure at the end of dev/logs/dev-api.log and report this problem to your user if there is no fixable startup failure in the log.`) + await assert.doesNotReject(anonymousAx.get(`http://${process.env.DEV_HOST}:${process.env.NGINX_PORT}/simple-directory`), + 'Simple Directory server seems to be unavailable. If you are agent do not try to fix this, instead report this problem to your user.') + + const tail = spawn('tail', ['-n', '0', '-f', 'dev/logs/dev-api.log'], { stdio: 'inherit', detached: true }) + process.env.TAIL_PID = tail.pid?.toString() +}) diff --git a/tests/state-teardown.ts b/tests/state-teardown.ts new file mode 100644 index 0000000..d9af52c --- /dev/null +++ b/tests/state-teardown.ts @@ -0,0 +1,7 @@ +import { test as teardown } from '@playwright/test' + +teardown('Stateful tests teardown', async () => { + if (process.env.TAIL_PID) { + try { process.kill(-Number(process.env.TAIL_PID)) } catch (e) { /* ignore */ } + } +}) diff --git a/test-it/02-subscriptions.ts b/tests/subscriptions.api.spec.ts similarity index 53% rename from test-it/02-subscriptions.ts rename to tests/subscriptions.api.spec.ts index da28cad..b08ef3e 100644 --- a/test-it/02-subscriptions.ts +++ b/tests/subscriptions.api.spec.ts @@ -1,211 +1,207 @@ -import { strict as assert } from 'node:assert' -import { it, describe, before, beforeEach, after } from 'node:test' +import { test, expect } from '@playwright/test' import WebSocket from 'ws' -import { axios, axiosAuth, clean, startApiServer, stopApiServer } from './utils/index.ts' +import { axios, axiosAuth, clean, devBaseURL } from './support/axios.ts' -const axPush = axios({ params: { key: 'SECRET_EVENTS' }, baseURL: 'http://localhost:8082/events' }) -const user1 = await axiosAuth('user1@test.com') -const user2 = await axiosAuth('user2@test.com') -const admin1 = await axiosAuth('admin1@test.com') +const axPush = axios({ params: { key: 'SECRET_EVENTS' }, baseURL: devBaseURL }) +const user1 = await axiosAuth('test-user1') +const user2 = await axiosAuth('test-user2') +const admin1 = await axiosAuth('test1-admin1') -describe('subscriptions', () => { - before(startApiServer) - beforeEach(clean) - after(stopApiServer) +test.describe('subscriptions', () => { + test.beforeEach(clean) - it('should reject wrong recipient', async () => { - await assert.rejects(user1.post('/api/subscriptions', { + test('should reject wrong recipient', async () => { + await expect(user1.post('/api/subscriptions', { topic: { key: 'topic1' }, recipient: { id: 'anotheruser' }, - sender: { type: 'user', id: 'user1' } - }), { status: 403 }) + sender: { type: 'user', id: 'test-user1' } + })).rejects.toMatchObject({ status: 403 }) }) - it('should send a public notification to any subscribed user', async () => { + test('should send a public notification to any subscribed user', async () => { const subscription = { topic: { key: 'topic1' }, - sender: { type: 'user', id: 'user1' }, + sender: { type: 'user', id: 'test-user1' }, visibility: 'public' } await admin1.post('/api/subscriptions', subscription) - await assert.rejects(admin1.post('/api/subscriptions', subscription), { status: 409 }) + await expect(admin1.post('/api/subscriptions', subscription)).rejects.toMatchObject({ status: 409 }) await user1.post('/api/subscriptions', subscription) await user2.post('/api/subscriptions', subscription) let res = await admin1.get('/api/subscriptions') - assert.equal(res.data.count, 1) - assert.equal(res.data.results[0].origin, 'http://localhost:5600') + expect(res.data.count).toBe(1) + expect(res.data.results[0].origin).toBe(`http://${process.env.DEV_HOST}:${process.env.NGINX_PORT}`) res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', body: 'a notification from host {hostname}', - sender: { type: 'user', id: 'user1', name: 'User 1' }, + sender: { type: 'user', id: 'test-user1', name: 'User 1' }, visibility: 'public' }]) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) - assert.equal(res.data.results[0].body, 'a notification from host localhost') + expect(res.data.count).toBe(1) + expect(res.data.results[0].body).toBe(`a notification from host ${process.env.DEV_HOST}`) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) }) - it('should send a private notification only to member of sender organization', async () => { + test('should send a private notification only to member of sender organization', async () => { const subscription = { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga1' } + sender: { type: 'organization', id: 'test1' } } let res = await admin1.post('/api/subscriptions', subscription) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await user1.post('/api/subscriptions', subscription) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await user2.post('/api/subscriptions', subscription) - assert.equal(res.data.visibility, 'public') + expect(res.data.visibility).toBe('public') res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', - sender: { type: 'organization', id: 'orga1', name: 'Orga 1' } + sender: { type: 'organization', id: 'test1', name: 'Test Organization 1' } }]) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 0) + expect(res.data.count).toBe(0) }) - it('should send a private notification only to member of sender organization with certain role', async () => { + test('should send a private notification only to member of sender organization with certain role', async () => { const subscription = { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga1', role: 'admin' } + sender: { type: 'organization', id: 'test1', role: 'admin' } } let res = await admin1.post('/api/subscriptions', subscription) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await user1.post('/api/subscriptions', subscription) - assert.equal(res.data.visibility, 'public') + expect(res.data.visibility).toBe('public') res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', - sender: { type: 'organization', id: 'orga1', role: 'admin', name: 'Orga 1' } + sender: { type: 'organization', id: 'test1', role: 'admin', name: 'Test Organization 1' } }]) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 0) + expect(res.data.count).toBe(0) }) - it('should not send a global private notification to member of a department in sender organization', async () => { + test('should not send a global private notification to member of a department in sender organization', async () => { const subscription = { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga2' } + sender: { type: 'organization', id: 'test2' } } let res = await user2.post('/api/subscriptions', subscription) - // user2 is in a department, he only as access to public notification on the root of the org - assert.equal(res.data.visibility, 'public') + expect(res.data.visibility).toBe('public') res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', - sender: { type: 'organization', id: 'orga2', name: 'Orga 2' } + sender: { type: 'organization', id: 'test2', name: 'Test Organization 2' } }]) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 0) + expect(res.data.count).toBe(0) }) - it('should send a notification to any department', async () => { + test('should send a notification to any department', async () => { let res = await user1.post('/api/subscriptions', { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga2', department: 'dep1', name: 'Orga 2' } + sender: { type: 'organization', id: 'test2', department: 'dep1', name: 'Test Organization 2' } }) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await user2.post('/api/subscriptions', { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga2', department: 'dep2', name: 'Orga 2' } + sender: { type: 'organization', id: 'test2', department: 'dep2', name: 'Test Organization 2' } }) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', - sender: { type: 'organization', id: 'orga2', department: '*', name: 'Orga 2' } + sender: { type: 'organization', id: 'test2', department: '*', name: 'Test Organization 2' } }]) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) }) - it('should send a private department notification to member of right department in sender organization', async () => { + test('should send a private department notification to member of right department in sender organization', async () => { let res = await user1.post('/api/subscriptions', { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga2', department: 'dep2' } + sender: { type: 'organization', id: 'test2', department: 'dep2' } }) - assert.equal(res.data.visibility, 'public') + expect(res.data.visibility).toBe('public') res = await user2.post('/api/subscriptions', { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga2', department: 'dep2' } + sender: { type: 'organization', id: 'test2', department: 'dep2' } }) - assert.equal(res.data.visibility, 'private') + expect(res.data.visibility).toBe('private') res = await axPush.post('/api/events', [{ date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'a notification', - sender: { type: 'organization', id: 'orga2', department: 'dep2', name: 'Orga 2' } + sender: { type: 'organization', id: 'test2', department: 'dep2', name: 'Test Organization 2' } }]) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 0) + expect(res.data.count).toBe(0) }) - it('should send a notification with subscribedOnly option', async () => { + test('should send a notification with subscribedOnly option', async () => { const notif = { topic: { key: 'topic1' }, title: 'a notification', body: 'a notification from host {hostname}', - sender: { type: 'user', id: 'user1', name: 'User 1' }, + sender: { type: 'user', id: 'test-user1', name: 'User 1' }, visibility: 'public', - recipient: { id: 'admin1' } + recipient: { id: 'test1-admin1' } } let res = await axPush.post('/api/notifications', notif, { params: { subscribedOnly: 'true' } }) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 0) + expect(res.data.count).toBe(0) const subscription = { topic: { key: 'topic1' }, - sender: { type: 'user', id: 'user1' }, + sender: { type: 'user', id: 'test-user1' }, visibility: 'public' } await admin1.post('/api/subscriptions', subscription) res = await axPush.post('/api/notifications', notif, { params: { subscribedOnly: 'true' } }) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) - assert.equal(res.data.results[0].body, 'a notification from host localhost') + expect(res.data.count).toBe(1) + expect(res.data.results[0].body).toBe(`a notification from host ${process.env.DEV_HOST}`) }) - it('should send a global public notification to any subscribed user', async () => { + test('should send a global public notification to any subscribed user', async () => { const subscription = { topic: { key: 'global-topic1' }, visibility: 'public' } await admin1.post('/api/subscriptions', subscription) - await assert.rejects(admin1.post('/api/subscriptions', subscription), { status: 409 }) + await expect(admin1.post('/api/subscriptions', subscription)).rejects.toMatchObject({ status: 409 }) await user1.post('/api/subscriptions', subscription) await user2.post('/api/subscriptions', subscription) let res = await admin1.get('/api/subscriptions') - assert.equal(res.data.count, 1) - assert.equal(res.data.results[0].origin, 'http://localhost:5600') + expect(res.data.count).toBe(1) + expect(res.data.results[0].origin).toBe(`http://${process.env.DEV_HOST}:${process.env.NGINX_PORT}`) res = await axPush.post('/api/events', [{ date: new Date().toISOString(), @@ -215,29 +211,29 @@ describe('subscriptions', () => { visibility: 'public' }]) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) - assert.equal(res.data.results[0].body, 'a global notification from host localhost') + expect(res.data.count).toBe(1) + expect(res.data.results[0].body).toBe(`a global notification from host ${process.env.DEV_HOST}`) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) res = await user2.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) }) - it('should send notificationss of de-duplicated events', async () => { + test('should send notifications of de-duplicated events', async () => { // user1 is subscribed in 2 different manners let res = await user1.post('/api/subscriptions', { topic: { key: 'topic1' }, - sender: { type: 'organization', id: 'orga1' } + sender: { type: 'organization', id: 'test1' } }) res = await user1.post('/api/subscriptions', { topic: { key: 'topic2' }, - sender: { type: 'organization', id: 'orga1' } + sender: { type: 'organization', id: 'test1' } }) // admin is subscribed in the second manner only res = await admin1.post('/api/subscriptions', { topic: { key: 'topic2' }, - sender: { type: 'organization', id: 'orga1' } + sender: { type: 'organization', id: 'test1' } }) res = await axPush.post('/api/events', [{ @@ -245,20 +241,20 @@ describe('subscriptions', () => { date: new Date().toISOString(), topic: { key: 'topic1' }, title: 'notif 1', - sender: { type: 'organization', id: 'orga1', name: 'Orga 1' } + sender: { type: 'organization', id: 'test1', name: 'Test Organization 1' } }]) res = await axPush.post('/api/events', [{ _id: 'test', date: new Date().toISOString(), topic: { key: 'topic2' }, title: 'notif 2', - sender: { type: 'organization', id: 'orga1', name: 'Orga 1' } + sender: { type: 'organization', id: 'test1', name: 'Test Organization 1' } }]) res = await admin1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) // no duplicate created res = await user1.get('/api/notifications') - assert.equal(res.data.count, 1) + expect(res.data.count).toBe(1) // another notification sent straight to the user res = await axPush.post('/api/notifications', { @@ -266,10 +262,10 @@ describe('subscriptions', () => { date: new Date().toISOString(), topic: { key: 'topic2' }, title: 'notif 2', - recipient: { id: 'user1' } + recipient: { id: 'test-user1' } }) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 2) + expect(res.data.count).toBe(2) // a duplicate not saved res = await axPush.post('/api/notifications', { @@ -277,15 +273,15 @@ describe('subscriptions', () => { date: new Date().toISOString(), topic: { key: 'topic2' }, title: 'notif 2', - recipient: { id: 'user1' } + recipient: { id: 'test-user1' } }) res = await user1.get('/api/notifications') - assert.equal(res.data.count, 2) + expect(res.data.count).toBe(2) }) - it('should deliver direct notifications via WS', async () => { - const cookies = user1.cookieJar.getCookiesSync('http://localhost:5600') - const ws = new WebSocket('ws://localhost:8082', { headers: { Cookie: cookies.map(String).join('; ') } }) + test('should deliver direct notifications via WS', async () => { + const cookies = user1.cookieJar.getCookiesSync(`http://${process.env.DEV_HOST}:${process.env.NGINX_PORT}`) + const ws = new WebSocket(`ws://localhost:${process.env.DEV_API_PORT}`, { headers: { Cookie: cookies.map(String).join('; ') } }) const messages: any[] = [] await new Promise