335 lines
12 KiB
TypeScript
335 lines
12 KiB
TypeScript
import { expect, test } from './fixtures';
|
|
import { EVENT_TYPE, UNKNOWN_UUID } from './helpers/constants';
|
|
import { dateRange, TIMEZONE } from './helpers/dates';
|
|
import { createWebsite, deleteWebsite } from './helpers/entities';
|
|
import { PERSONAS } from './seed/dataset';
|
|
|
|
const sessionShape = {
|
|
id: expect.any(String),
|
|
websiteId: expect.any(String),
|
|
firstAt: expect.any(String),
|
|
lastAt: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
visits: expect.anything(),
|
|
views: expect.anything(),
|
|
events: expect.anything(),
|
|
};
|
|
|
|
test.describe('Sessions', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
/** A seeded session that carries a distinct id (identified persona). */
|
|
let identifiedSessionId = '';
|
|
|
|
test('GET /api/websites/{websiteId}/sessions lists sessions with paging', async ({
|
|
admin,
|
|
share,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions`;
|
|
const response = await admin.get(path, { params: dateRange(seed, { pageSize: 5 }) });
|
|
const second = await admin.get(path, { params: dateRange(seed, { pageSize: 5, page: 2 }) });
|
|
const shared = await (await share()).get(path, { params: dateRange(seed) });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({
|
|
data: expect.any(Array),
|
|
count: expect.any(Number),
|
|
page: 1,
|
|
pageSize: 5,
|
|
});
|
|
expect(response.body.data).toHaveLength(5);
|
|
expect(response.body.count).toBeGreaterThan(5);
|
|
expect(response.body.data[0]).toMatchObject({ ...sessionShape, websiteId: seed.website.id });
|
|
// `distinctId` is omitted from rows for anonymous sessions.
|
|
for (const key of ['browser', 'os', 'device', 'screen', 'language', 'country']) {
|
|
expect(response.body.data[0]).toHaveProperty(key);
|
|
}
|
|
expect(response.body.data.map((s: any) => s.id)).not.toContain(second.body.data[0].id);
|
|
expect(shared.status).toBe(200);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions filters by distinctId and search', async ({
|
|
admin,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions`;
|
|
const [distinctId] = seed.data.distinctIds;
|
|
const unfiltered = await admin.get(path, { params: dateRange(seed) });
|
|
const filtered = await admin.get(path, { params: dateRange(seed, { distinctId }) });
|
|
const searched = await admin.get(path, { params: dateRange(seed, { search: distinctId }) });
|
|
const none = await admin.get(path, {
|
|
params: dateRange(seed, { distinctId: 'zzz-no-such-user' }),
|
|
});
|
|
|
|
expect(filtered.status).toBe(200);
|
|
expect(filtered.body.data.length).toBeGreaterThan(0);
|
|
expect(filtered.body.count).toBeLessThan(unfiltered.body.count);
|
|
expect(filtered.body.data[0]).toMatchObject({ ...sessionShape, websiteId: seed.website.id });
|
|
expect(searched.status).toBe(200);
|
|
expect(searched.body.data.length).toBeGreaterThan(0);
|
|
expect(none.body.data).toHaveLength(0);
|
|
expect(none.body.count).toBe(0);
|
|
|
|
// Only the session that received the `identify` call carries session data.
|
|
for (const session of filtered.body.data) {
|
|
const properties = await admin.get(`${path}/${session.id}/properties`);
|
|
|
|
if (properties.body?.length) {
|
|
identifiedSessionId = session.id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
expect(identifiedSessionId).not.toBe('');
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions validates params and permissions', async ({
|
|
admin,
|
|
api,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions`;
|
|
const missingRange = await admin.get(path);
|
|
const anonymous = await api.get(path, { params: dateRange(seed) });
|
|
const denied = await viewer.get(path, { params: dateRange(seed) });
|
|
const unknown = await viewer.get(`/api/websites/${UNKNOWN_UUID}/sessions`, {
|
|
params: dateRange(seed),
|
|
});
|
|
|
|
expect(missingRange.status).toBe(400);
|
|
expect(anonymous.status).toBe(401);
|
|
expect(denied.status).toBe(401);
|
|
expect(unknown.status).toBe(401);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions/{sessionId} returns a session', async ({
|
|
admin,
|
|
share,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions/${seed.data.sessionId}`;
|
|
const response = await admin.get(path);
|
|
const identified = await admin.get(
|
|
`/api/websites/${seed.website.id}/sessions/${identifiedSessionId}`,
|
|
);
|
|
const shared = await (await share()).get(path);
|
|
const denied = await viewer.get(path);
|
|
const unknown = await admin.get(`/api/websites/${seed.website.id}/sessions/${UNKNOWN_UUID}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toMatchObject({
|
|
id: seed.data.sessionId,
|
|
websiteId: seed.website.id,
|
|
canDelete: seed.db === 'postgres',
|
|
stitchedSessionCount: expect.any(Number),
|
|
});
|
|
expect(response.body.stitchedSessionCount).toBeGreaterThanOrEqual(1);
|
|
|
|
expect(identified.status).toBe(200);
|
|
expect(identified.body.distinctId).toBe(seed.data.distinctIds[0]);
|
|
expect(identified.body.stitchedSessionCount).toBeGreaterThanOrEqual(1);
|
|
|
|
expect(shared.status).toBe(200);
|
|
expect(shared.body.id).toBe(seed.data.sessionId);
|
|
expect(shared.body.canDelete).toBe(false);
|
|
expect(denied.status).toBe(401);
|
|
expect(unknown.status).toBe(404);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions/{sessionId}/activity returns the event log', async ({
|
|
admin,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions/${seed.data.sessionId}/activity`;
|
|
const response = await admin.get(path, { params: dateRange(seed) });
|
|
const stitched = await admin.get(
|
|
`/api/websites/${seed.website.id}/sessions/${identifiedSessionId}/activity`,
|
|
{ params: dateRange(seed, { distinctId: seed.data.distinctIds[0] }) },
|
|
);
|
|
const missingRange = await admin.get(path);
|
|
const invalid = await admin.get(path, { params: { startAt: 'abc', endAt: 'def' } });
|
|
const denied = await viewer.get(path, { params: dateRange(seed) });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(Array.isArray(response.body)).toBe(true);
|
|
expect(response.body.length).toBeGreaterThan(0);
|
|
expect(response.body[0]).toMatchObject({
|
|
eventId: expect.any(String),
|
|
visitId: expect.any(String),
|
|
createdAt: expect.any(String),
|
|
urlPath: expect.any(String),
|
|
eventType: expect.any(Number),
|
|
});
|
|
expect(response.body[0]).toHaveProperty('eventName');
|
|
expect(response.body[0]).toHaveProperty('hasData');
|
|
expect(response.body.some((row: any) => row.eventType === EVENT_TYPE.pageView)).toBe(true);
|
|
|
|
expect(stitched.status).toBe(200);
|
|
expect(stitched.body.length).toBeGreaterThan(0);
|
|
expect(missingRange.status).toBe(400);
|
|
expect(invalid.status).toBe(400);
|
|
expect(denied.status).toBe(401);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions/{sessionId}/properties returns session data', async ({
|
|
admin,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions/${identifiedSessionId}/properties`;
|
|
const response = await admin.get(path);
|
|
const unknown = await admin.get(
|
|
`/api/websites/${seed.website.id}/sessions/${UNKNOWN_UUID}/properties`,
|
|
);
|
|
const denied = await viewer.get(path);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(Array.isArray(response.body)).toBe(true);
|
|
expect(response.body.length).toBeGreaterThan(0);
|
|
expect(response.body[0]).toMatchObject({
|
|
sessionId: identifiedSessionId,
|
|
dataKey: expect.any(String),
|
|
dataType: expect.any(Number),
|
|
});
|
|
for (const key of ['stringValue', 'numberValue', 'dateValue']) {
|
|
expect(response.body[0]).toHaveProperty(key);
|
|
}
|
|
expect(unknown.status).toBe(200);
|
|
expect(unknown.body).toEqual([]);
|
|
expect(denied.status).toBe(401);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions/stats returns session totals', async ({
|
|
admin,
|
|
share,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions/stats`;
|
|
const response = await admin.get(path, { params: dateRange(seed) });
|
|
const missingRange = await admin.get(path);
|
|
const shared = await (await share()).get(path, { params: dateRange(seed) });
|
|
const denied = await viewer.get(path, { params: dateRange(seed) });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({
|
|
pageviews: { value: expect.anything() },
|
|
visitors: { value: expect.anything() },
|
|
visits: { value: expect.anything() },
|
|
countries: { value: expect.anything() },
|
|
events: { value: expect.anything() },
|
|
});
|
|
expect(Number(response.body.pageviews.value)).toBeGreaterThanOrEqual(
|
|
seed.data.expectedPageviews,
|
|
);
|
|
expect(Number(response.body.visitors.value)).toBeGreaterThan(0);
|
|
expect(Number(response.body.events.value)).toBeGreaterThan(0);
|
|
expect(missingRange.status).toBe(400);
|
|
expect(shared.status).toBe(200);
|
|
expect(denied.status).toBe(401);
|
|
});
|
|
|
|
test('GET /api/websites/{websiteId}/sessions/weekly returns a 7x24 grid', async ({
|
|
admin,
|
|
share,
|
|
viewer,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${seed.website.id}/sessions/weekly`;
|
|
const params = dateRange(seed, { timezone: TIMEZONE });
|
|
const response = await admin.get(path, { params });
|
|
const missingTimezone = await admin.get(path, { params: dateRange(seed) });
|
|
const shared = await (await share()).get(path, { params });
|
|
const denied = await viewer.get(path, { params });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toHaveLength(7);
|
|
for (const day of response.body) {
|
|
expect(day).toHaveLength(24);
|
|
for (const hour of day) {
|
|
expect(typeof hour).toBe('number');
|
|
}
|
|
}
|
|
const total = response.body.flat().reduce((sum: number, n: number) => sum + n, 0);
|
|
expect(total).toBeGreaterThan(0);
|
|
expect(missingTimezone.status).toBe(400);
|
|
expect(shared.status).toBe(200);
|
|
expect(denied.status).toBe(401);
|
|
});
|
|
|
|
test.describe('DELETE /api/websites/{websiteId}/sessions/{sessionId}', () => {
|
|
let websiteId = '';
|
|
let sessionId = '';
|
|
|
|
test.beforeAll(async ({ admin, api, seed }) => {
|
|
test.skip(seed.db === 'clickhouse', 'Session deletion requires relational storage');
|
|
|
|
websiteId = (await createWebsite(admin)).id;
|
|
|
|
const sent = await api.post('/api/send', {
|
|
type: 'event',
|
|
payload: {
|
|
website: websiteId,
|
|
hostname: 'x.test',
|
|
url: '/',
|
|
ip: '10.0.9.9',
|
|
userAgent: PERSONAS[0].userAgent,
|
|
},
|
|
});
|
|
|
|
expect(sent.status).toBe(200);
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const sessions = await admin.get(`/api/websites/${websiteId}/sessions`, {
|
|
params: { startAt: Date.now() - 60 * 60 * 1000, endAt: Date.now() },
|
|
});
|
|
sessionId = sessions.body?.data?.[0]?.id ?? '';
|
|
return sessionId;
|
|
},
|
|
{ timeout: 15_000 },
|
|
)
|
|
.not.toBe('');
|
|
});
|
|
|
|
test.afterAll(async ({ admin }) => {
|
|
if (websiteId) {
|
|
await deleteWebsite(admin, websiteId);
|
|
}
|
|
});
|
|
|
|
test('DELETE /api/websites/{websiteId}/sessions/{sessionId} deletes a session', async ({
|
|
admin,
|
|
api,
|
|
user,
|
|
seed,
|
|
}) => {
|
|
const path = `/api/websites/${websiteId}/sessions/${sessionId}`;
|
|
const anonymous = await api.del(path);
|
|
const denied = await user.del(path);
|
|
const before = await admin.get(path);
|
|
const response = await admin.del(path);
|
|
const after = await admin.get(path);
|
|
const unknown = await admin.del(`/api/websites/${websiteId}/sessions/${UNKNOWN_UUID}`);
|
|
const list = await admin.get(`/api/websites/${websiteId}/sessions`, {
|
|
params: dateRange(seed),
|
|
});
|
|
|
|
expect(anonymous.status).toBe(401);
|
|
expect(denied.status).toBe(401);
|
|
expect(before.status).toBe(200);
|
|
expect(before.body.canDelete).toBe(true);
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual({ ok: true });
|
|
expect(after.status).toBe(404);
|
|
expect(unknown.status).toBe(404);
|
|
expect(list.body.data).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|