mirror of
https://github.com/directus/directus.git
synced 2026-01-22 20:47:54 -05:00
Add cookie logger tests (#17932)
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch> Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ TODO
|
||||
debug
|
||||
debug.ts
|
||||
.clinic
|
||||
/uploads
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './functions';
|
||||
export * from './seed-functions';
|
||||
export * from './types';
|
||||
export * from './transport';
|
||||
export * from './test-logger';
|
||||
|
||||
61
tests-blackbox/common/test-logger.ts
Normal file
61
tests-blackbox/common/test-logger.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ChildProcess } from 'child_process';
|
||||
|
||||
export class TestLogger {
|
||||
private logs: string;
|
||||
private server: ChildProcess;
|
||||
private stopCondition: string;
|
||||
private filterCondition?: string;
|
||||
private stopped?: boolean;
|
||||
private resolve?: (log: string) => void;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param server Process running a Directus instance
|
||||
* @param stopCondition Finish as soon as the specified string appears in the logs.
|
||||
* @param filter Only capture log chunks containing the specified string, if `true` uses the same string as defined for `stopCondition`
|
||||
*/
|
||||
constructor(server: ChildProcess, stopCondition: string, filterCondition?: boolean | string) {
|
||||
this.logs = '';
|
||||
this.server = server;
|
||||
this.stopCondition = stopCondition;
|
||||
if (filterCondition) {
|
||||
this.filterCondition = filterCondition === true ? stopCondition : filterCondition;
|
||||
}
|
||||
|
||||
// Discard data up to this point
|
||||
server.stdout?.read();
|
||||
|
||||
server.stdout?.on('data', this.processChunks);
|
||||
}
|
||||
|
||||
private processChunks = (chunk: any) => {
|
||||
const logLine = String(chunk);
|
||||
|
||||
if (!this.stopped && (!this.filterCondition || logLine.includes(this.filterCondition))) {
|
||||
this.logs += logLine;
|
||||
}
|
||||
|
||||
if (this.logs.includes(this.stopCondition)) {
|
||||
this.stopped = true;
|
||||
this.cleanup();
|
||||
|
||||
if (this.resolve) {
|
||||
this.resolve(this.logs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getLogs = () => {
|
||||
return new Promise<string>((resolve) => {
|
||||
if (this.stopped) {
|
||||
resolve(this.logs);
|
||||
} else {
|
||||
this.resolve = resolve;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cleanup = () => {
|
||||
this.server.stdout?.off('data', this.processChunks);
|
||||
};
|
||||
}
|
||||
@@ -10,16 +10,17 @@ export async function requestGraphQL(
|
||||
isSystemCollection: boolean,
|
||||
token: string | null,
|
||||
jsonQuery: any,
|
||||
variables?: any
|
||||
options?: { variables?: any; cookies?: string[] }
|
||||
): Promise<any> {
|
||||
const req = request(host)
|
||||
.post(isSystemCollection ? '/graphql/system' : '/graphql')
|
||||
.send({
|
||||
query: processGraphQLJson(jsonQuery),
|
||||
variables,
|
||||
variables: options?.variables,
|
||||
});
|
||||
|
||||
if (token) req.set('Authorization', `Bearer ${token}`);
|
||||
if (options?.cookies) req.set('Cookie', options.cookies);
|
||||
|
||||
return await req;
|
||||
}
|
||||
|
||||
262
tests-blackbox/logger/redact.test.ts
Normal file
262
tests-blackbox/logger/redact.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import config, { getUrl } from '@common/config';
|
||||
import vendors from '@common/get-dbs-to-test';
|
||||
import * as common from '@common/index';
|
||||
import { TestLogger } from '@common/test-logger';
|
||||
import { awaitDirectusConnection } from '@utils/await-connection';
|
||||
import { ChildProcess, spawn } from 'child_process';
|
||||
import { EnumType } from 'json-to-graphql-query';
|
||||
import knex, { Knex } from 'knex';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import request from 'supertest';
|
||||
|
||||
describe('Logger Redact Tests', () => {
|
||||
const databases = new Map<string, Knex>();
|
||||
const directusInstances = {} as { [vendor: string]: ChildProcess };
|
||||
const env = cloneDeep(config.envs);
|
||||
const authModes = ['json', 'cookie'];
|
||||
|
||||
for (const vendor of vendors) {
|
||||
env[vendor].LOG_STYLE = 'raw';
|
||||
env[vendor].LOG_LEVEL = 'info';
|
||||
env[vendor].PORT = String(Number(env[vendor]!.PORT) + 500);
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const promises = [];
|
||||
|
||||
for (const vendor of vendors) {
|
||||
databases.set(vendor, knex(config.knexConfig[vendor]!));
|
||||
|
||||
const server = spawn('node', ['api/cli', 'start'], { env: env[vendor] });
|
||||
directusInstances[vendor] = server;
|
||||
|
||||
promises.push(awaitDirectusConnection(Number(env[vendor].PORT)));
|
||||
}
|
||||
|
||||
// Give the server some time to start
|
||||
await Promise.all(promises);
|
||||
}, 180000);
|
||||
|
||||
afterAll(async () => {
|
||||
for (const [vendor, connection] of databases) {
|
||||
directusInstances[vendor]!.kill();
|
||||
|
||||
await connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
describe('POST /refresh', () => {
|
||||
describe('refreshes with refresh_token in the body', () => {
|
||||
describe.each(authModes)('for %s mode', (mode) => {
|
||||
common.TEST_USERS.forEach((userKey) => {
|
||||
describe(common.USER[userKey].NAME, () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Setup
|
||||
const refreshToken = (
|
||||
await request(getUrl(vendor, env))
|
||||
.post(`/auth/login`)
|
||||
.send({ email: common.USER[userKey].EMAIL, password: common.USER[userKey].PASSWORD })
|
||||
.expect('Content-Type', /application\/json/)
|
||||
).body.data.refresh_token;
|
||||
|
||||
const refreshToken2 = (
|
||||
await common.requestGraphQL(getUrl(vendor, env), true, null, {
|
||||
mutation: {
|
||||
auth_login: {
|
||||
__args: {
|
||||
email: common.USER[userKey].EMAIL,
|
||||
password: common.USER[userKey].PASSWORD,
|
||||
},
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
).body.data.auth_login.refresh_token;
|
||||
|
||||
// Action
|
||||
const logger = new TestLogger(directusInstances[vendor], '/auth/refresh', true);
|
||||
|
||||
const response = await request(getUrl(vendor, env))
|
||||
.post(`/auth/refresh`)
|
||||
.send({ refresh_token: refreshToken, mode })
|
||||
.expect('Content-Type', /application\/json/);
|
||||
|
||||
const logs = await logger.getLogs();
|
||||
|
||||
const loggerGql = new TestLogger(directusInstances[vendor], '/graphql/system', true);
|
||||
|
||||
const mutationKey = 'auth_refresh';
|
||||
|
||||
const gqlResponse = await common.requestGraphQL(getUrl(vendor, env), true, null, {
|
||||
mutation: {
|
||||
[mutationKey]: {
|
||||
__args: {
|
||||
refresh_token: refreshToken2,
|
||||
mode: new EnumType(mode),
|
||||
},
|
||||
access_token: true,
|
||||
expires: true,
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const logsGql = await loggerGql.getLogs();
|
||||
|
||||
// Assert
|
||||
expect(response.statusCode).toBe(200);
|
||||
if (mode === 'cookie') {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
},
|
||||
});
|
||||
|
||||
for (const log of [logs, logsGql]) {
|
||||
expect((log.match(/"cookie":"--redact--"/g) || []).length).toBe(0);
|
||||
expect((log.match(/"set-cookie":"--redact--"/g) || []).length).toBe(1);
|
||||
}
|
||||
} else {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
});
|
||||
|
||||
for (const log of [logs, logsGql]) {
|
||||
expect((log.match(/"cookie":"--redact--"/g) || []).length).toBe(0);
|
||||
expect((log.match(/"set-cookie":"--redact--"/g) || []).length).toBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
expect(gqlResponse.statusCode).toBe(200);
|
||||
expect(gqlResponse.body).toMatchObject({
|
||||
data: {
|
||||
[mutationKey]: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshes with refresh_token in the cookie', () => {
|
||||
describe.each(authModes)('for %s mode', (mode) => {
|
||||
common.TEST_USERS.forEach((userKey) => {
|
||||
describe(common.USER[userKey].NAME, () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Setup
|
||||
const cookieName = 'directus_refresh_token';
|
||||
|
||||
const refreshToken = (
|
||||
await request(getUrl(vendor, env))
|
||||
.post(`/auth/login`)
|
||||
.send({ email: common.USER[userKey].EMAIL, password: common.USER[userKey].PASSWORD })
|
||||
.expect('Content-Type', /application\/json/)
|
||||
).body.data.refresh_token;
|
||||
|
||||
const refreshToken2 = (
|
||||
await common.requestGraphQL(getUrl(vendor, env), true, null, {
|
||||
mutation: {
|
||||
auth_login: {
|
||||
__args: {
|
||||
email: common.USER[userKey].EMAIL,
|
||||
password: common.USER[userKey].PASSWORD,
|
||||
},
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
).body.data.auth_login.refresh_token;
|
||||
|
||||
// Action
|
||||
const logger = new TestLogger(directusInstances[vendor], '/auth/refresh', true);
|
||||
|
||||
const response = await request(getUrl(vendor, env))
|
||||
.post(`/auth/refresh`)
|
||||
.set('Cookie', `${cookieName}=${refreshToken}`)
|
||||
.send({ mode })
|
||||
.expect('Content-Type', /application\/json/);
|
||||
|
||||
const logs = await logger.getLogs();
|
||||
|
||||
const loggerGql = new TestLogger(directusInstances[vendor], '/graphql/system', true);
|
||||
|
||||
const mutationKey = 'auth_refresh';
|
||||
|
||||
const gqlResponse = await common.requestGraphQL(
|
||||
getUrl(vendor, env),
|
||||
true,
|
||||
null,
|
||||
{
|
||||
mutation: {
|
||||
[mutationKey]: {
|
||||
__args: {
|
||||
refresh_token: refreshToken2,
|
||||
mode: new EnumType(mode),
|
||||
},
|
||||
access_token: true,
|
||||
expires: true,
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ cookies: [`${cookieName}=${refreshToken2}`] }
|
||||
);
|
||||
|
||||
const logsGql = await loggerGql.getLogs();
|
||||
|
||||
// Assert
|
||||
expect(response.statusCode).toBe(200);
|
||||
if (mode === 'cookie') {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
},
|
||||
});
|
||||
|
||||
for (const log of [logs, logsGql]) {
|
||||
expect((log.match(/"cookie":"--redact--"/g) || []).length).toBe(1);
|
||||
expect((log.match(/"set-cookie":"--redact--"/g) || []).length).toBe(1);
|
||||
}
|
||||
} else {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
});
|
||||
|
||||
for (const log of [logs, logsGql]) {
|
||||
expect((log.match(/"cookie":"--redact--"/g) || []).length).toBe(1);
|
||||
expect((log.match(/"set-cookie":"--redact--"/g) || []).length).toBe(0);
|
||||
}
|
||||
}
|
||||
|
||||
expect(gqlResponse.statusCode).toBe(200);
|
||||
expect(gqlResponse.body).toMatchObject({
|
||||
data: {
|
||||
[mutationKey]: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
189
tests-blackbox/routes/auth/refresh.test.ts
Normal file
189
tests-blackbox/routes/auth/refresh.test.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { getUrl } from '@common/config';
|
||||
import * as common from '@common/index';
|
||||
import request from 'supertest';
|
||||
import vendors from '@common/get-dbs-to-test';
|
||||
import { requestGraphQL } from '@common/index';
|
||||
import { EnumType } from 'json-to-graphql-query';
|
||||
|
||||
const authModes = ['json', 'cookie'];
|
||||
|
||||
describe('Authentication Refresh Tests', () => {
|
||||
describe('POST /refresh', () => {
|
||||
describe('refreshes with refresh_token in the body', () => {
|
||||
describe.each(authModes)('for %s mode', (mode) => {
|
||||
common.TEST_USERS.forEach((userKey) => {
|
||||
describe(common.USER[userKey].NAME, () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Setup
|
||||
const refreshToken = (
|
||||
await request(getUrl(vendor))
|
||||
.post(`/auth/login`)
|
||||
.send({ email: common.USER[userKey].EMAIL, password: common.USER[userKey].PASSWORD })
|
||||
.expect('Content-Type', /application\/json/)
|
||||
).body.data.refresh_token;
|
||||
|
||||
const refreshToken2 = (
|
||||
await requestGraphQL(getUrl(vendor), true, null, {
|
||||
mutation: {
|
||||
auth_login: {
|
||||
__args: {
|
||||
email: common.USER[userKey].EMAIL,
|
||||
password: common.USER[userKey].PASSWORD,
|
||||
},
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
).body.data.auth_login.refresh_token;
|
||||
|
||||
// Action
|
||||
const response = await request(getUrl(vendor))
|
||||
.post(`/auth/refresh`)
|
||||
.send({ refresh_token: refreshToken, mode })
|
||||
.expect('Content-Type', /application\/json/);
|
||||
|
||||
const mutationKey = 'auth_refresh';
|
||||
|
||||
const gqlResponse = await requestGraphQL(getUrl(vendor), true, null, {
|
||||
mutation: {
|
||||
[mutationKey]: {
|
||||
__args: {
|
||||
refresh_token: refreshToken2,
|
||||
mode: new EnumType(mode),
|
||||
},
|
||||
access_token: true,
|
||||
expires: true,
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(response.statusCode).toBe(200);
|
||||
if (mode === 'cookie') {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
expect(gqlResponse.statusCode).toBe(200);
|
||||
expect(gqlResponse.body).toMatchObject({
|
||||
data: {
|
||||
[mutationKey]: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshes with refresh_token in the cookie', () => {
|
||||
describe.each(authModes)('for %s mode', (mode) => {
|
||||
common.TEST_USERS.forEach((userKey) => {
|
||||
describe(common.USER[userKey].NAME, () => {
|
||||
it.each(vendors)('%s', async (vendor) => {
|
||||
// Setup
|
||||
const cookieName = 'directus_refresh_token';
|
||||
|
||||
const refreshToken = (
|
||||
await request(getUrl(vendor))
|
||||
.post(`/auth/login`)
|
||||
.send({ email: common.USER[userKey].EMAIL, password: common.USER[userKey].PASSWORD })
|
||||
.expect('Content-Type', /application\/json/)
|
||||
).body.data.refresh_token;
|
||||
|
||||
const refreshToken2 = (
|
||||
await requestGraphQL(getUrl(vendor), true, null, {
|
||||
mutation: {
|
||||
auth_login: {
|
||||
__args: {
|
||||
email: common.USER[userKey].EMAIL,
|
||||
password: common.USER[userKey].PASSWORD,
|
||||
},
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
).body.data.auth_login.refresh_token;
|
||||
|
||||
// Action
|
||||
const response = await request(getUrl(vendor))
|
||||
.post(`/auth/refresh`)
|
||||
.set('Cookie', `${cookieName}=${refreshToken}`)
|
||||
.send({ mode })
|
||||
.expect('Content-Type', /application\/json/);
|
||||
|
||||
const mutationKey = 'auth_refresh';
|
||||
|
||||
const gqlResponse = await requestGraphQL(
|
||||
getUrl(vendor),
|
||||
true,
|
||||
null,
|
||||
{
|
||||
mutation: {
|
||||
[mutationKey]: {
|
||||
__args: {
|
||||
refresh_token: refreshToken2,
|
||||
mode: new EnumType(mode),
|
||||
},
|
||||
access_token: true,
|
||||
expires: true,
|
||||
refresh_token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ cookies: [`${cookieName}=${refreshToken2}`] }
|
||||
);
|
||||
|
||||
// Assert
|
||||
expect(response.statusCode).toBe(200);
|
||||
if (mode === 'cookie') {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
expect(response.body).toMatchObject({
|
||||
data: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(Number),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
expect(gqlResponse.statusCode).toBe(200);
|
||||
expect(gqlResponse.body).toMatchObject({
|
||||
data: {
|
||||
[mutationKey]: {
|
||||
access_token: expect.any(String),
|
||||
expires: expect.any(String),
|
||||
refresh_token: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ exports.list = {
|
||||
{ testFilePath: '/schema/timezone/timezone.test.ts' },
|
||||
{ testFilePath: '/schema/timezone/timezone-changed-node-tz-america.test.ts' },
|
||||
{ testFilePath: '/schema/timezone/timezone-changed-node-tz-asia.test.ts' },
|
||||
{ testFilePath: '/logger/redact.test.ts' },
|
||||
{ testFilePath: '/routes/collections/schema-cache.test.ts' },
|
||||
{ testFilePath: '/routes/assets/concurrency.test.ts' },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user