Skip to content

Commit

Permalink
test(cubesql): Introduce E2E tests for SQL API
Browse files Browse the repository at this point in the history
  • Loading branch information
ovr committed Aug 10, 2022
1 parent 8a16cc7 commit 990802c
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 13 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/rust-cubesql.yml
Expand Up @@ -174,11 +174,16 @@ jobs:
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
run: cd packages/cubejs-backend-native && yarn run native:build
- name: Test native
- name: Test native (GNU only)
if: (matrix.target == 'x86_64-unknown-linux-gnu')
env:
CUBEJS_NATIVE_INTERNAL_DEBUG: true
run: cd packages/cubejs-backend-native && yarn run test:unit
- name: Run E2E Smoke testing over whole Cube (GNU only)
if: (matrix.target == 'x86_64-unknown-linux-gnu')
env:
CUBEJS_NATIVE_INTERNAL_DEBUG: true
run: yarn tsc && cd packages/cubejs-testing && yarn smoke:cubesql

native:
needs: [lint,unit,unit_legacy]
Expand Down
@@ -0,0 +1,25 @@
cube(`SecurityContextTest`, {
sql: `
SELECT r.user, r.uid FROM (
select 'admin' as user, 1 as uid
UNION ALL
select 'moderator' as user, 2 as uid
UNION ALL
select 'usr1' as user, 3 as uid
UNION ALL
select 'usr2' as user, 4 as uid
) as r
WHERE ${SECURITY_CONTEXT.user.requiredFilter('r.user')}
`,

dimensions: {
user: {
sql: `user`,
type: `string`,
},
uid: {
sql: `uid`,
type: `string`,
},
},
});
@@ -0,0 +1,69 @@
// Cube.js configuration options: https://cube.dev/docs/config
// It's a special configuration file for SQL API smoke's testing
module.exports = {
queryRewrite: (query, { securityContext }) => {
if (!securityContext.user) {
throw new Error('Property user does not exist in in Security Context!');
}

console.log('queryRewrite', {
securityContext
});

return query;
},
checkSqlAuth: async (req, user) => {
if (user === 'admin') {
return {
password: 'admin_password',
superuser: true,
securityContext: {
user: 'admin'
},
};
}

if (user === 'moderator') {
return {
password: 'moderator_password',
securityContext: {
user: 'moderator'
},
};
}

if (user === 'usr1') {
return {
password: 'user1_password',
securityContext: {
user: 'usr1'
},
};
}

if (user === 'usr2') {
return {
password: 'ignore password',
securityContext: {
user: 'usr2'
},
};
}

throw new Error(`User "${user}" doesn't exist`);
},
// ADMIN is allowed to access with superuser: true
// moderator is allowed to access -> user1/usr2
// usr1/usr2 are not allowed to change
canSwitchSqlUser: async (current, user) => {
await new Promise((resolve) => {
setTimeout(resolve, 1000);
});

if (current === 'moderator') {
return user === 'usr1';
}

return false;
}
};
9 changes: 6 additions & 3 deletions packages/cubejs-testing/package.json
Expand Up @@ -59,7 +59,9 @@
"smoke:multidb": "jest --verbose -i dist/test/smoke-multidb.test.js",
"smoke:multidb:snapshot": "jest --verbose --updateSnapshot -i dist/test/smoke-multidb.test.js",
"smoke:redshift": "jest --verbose -i dist/test/smoke-redshift.test.js",
"smoke:redshift:snapshot": "jest --verbose --updateSnapshot -i dist/test/smoke-redshift.test.js"
"smoke:redshift:snapshot": "jest --verbose --updateSnapshot -i dist/test/smoke-redshift.test.js",
"smoke:cubesql": "jest --verbose --forceExit -i dist/test/smoke-cubesql.test.js",
"smoke:cubesql:snapshot": "jest --verbose --forceExit --updateSnapshot -i dist/test/smoke-cubesql.test.js"
},
"files": [
"dist/src",
Expand Down Expand Up @@ -88,7 +90,7 @@
"@types/dedent": "^0.7.0",
"@types/http-proxy": "^1.17.5",
"@types/jest": "^26.0.22",
"@types/node": "^10.17.55",
"@types/node": "^12",
"cypress": "6.9.1",
"cypress-image-snapshot": "^4.0.1",
"cypress-localstorage-commands": "^1.4.5",
Expand All @@ -97,7 +99,8 @@
"eslint-plugin-cypress": "^2.12.1",
"jest": "^26.6.3",
"jwt-decode": "^3.1.2",
"typescript": "~4.1.5"
"typescript": "~4.1.5",
"pg": "^8.7.3"
},
"jest": {
"coveragePathIgnorePatterns": [
Expand Down
18 changes: 13 additions & 5 deletions packages/cubejs-testing/src/birdbox.ts
Expand Up @@ -64,15 +64,22 @@ export interface LocalOptions extends ContainerOptions {
/**
* Birdbox environments for cube.js passed for testcase.
*/
export interface Env {
export type Env = {
CUBEJS_DEV_MODE: string,
CUBEJS_WEB_SOCKETS: string,
CUBEJS_EXTERNAL_DEFAULT: string,
CUBEJS_SCHEDULED_REFRESH_DEFAULT: string,
CUBEJS_REFRESH_WORKER: string,
CUBEJS_ROLLUP_ONLY: string,
// SQL API
CUBEJS_SQL_PORT?: string,
CUBEJS_SQL_USER?: string,
CUBEJS_PG_SQL_PORT?: string,
CUBEJS_SQL_PASSWORD?: string,
CUBEJS_SQL_SUPER_USER?: string,
} & {
[key: string]: string,
}
};

/**
* List of permanent test data files.
Expand Down Expand Up @@ -212,7 +219,7 @@ export async function startBirdBoxFromContainer(
`[Birdbox] Using ${composeFile} compose file\n`
);
}

const env = await dc
.withStartupTimeout(30 * 1000)
.withEnv(
Expand All @@ -236,7 +243,7 @@ export async function startBirdBoxFromContainer(
`[Birdbox] Creating a proxy server 4000->${port} for local testing\n`
);
}

// As local Playground proxies requests to the 4000 port
proxyServer = HttpProxy.createProxyServer({
target: `http://localhost:${port}`
Expand Down Expand Up @@ -321,10 +328,11 @@ export async function startBirdBoxFromCli(
if (!options.schemaDir) {
options.schemaDir = 'postgresql/schema';
}

if (!options.cubejsConfig) {
options.cubejsConfig = 'postgresql/single/cube.js';
}

if (options.loadScript) {
db = await PostgresDBRunner.startContainer({
volumes: [
Expand Down
@@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SQL API Postgres (Data) SELECT COUNT(*) as cn, "status" FROM Orders GROUP BY 2 ORDER BY cn DESC: sql_orders 1`] = `
Array [
Object {
"cn": "2",
"status": "processed",
},
Object {
"cn": "2",
"status": "new",
},
Object {
"cn": "1",
"status": "shipped",
},
]
`;
168 changes: 168 additions & 0 deletions packages/cubejs-testing/test/smoke-cubesql.test.ts
@@ -0,0 +1,168 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { afterAll, beforeAll, jest, expect } from '@jest/globals';
import { Client as PgClient } from 'pg';
import { PostgresDBRunner } from '@cubejs-backend/testing-shared';
import type { StartedTestContainer } from 'testcontainers';

import { BirdBox, getBirdbox } from '../src';
import { DEFAULT_CONFIG } from './smoke-tests';

describe('SQL API', () => {
jest.setTimeout(60 * 5 * 1000);

let connection: PgClient;
let birdbox: BirdBox;
let db: StartedTestContainer;

// TODO: Random port?
const pgPort = 5656;
let connectionId = 0;

async function createPostgresClient(user: string, password: string) {
connectionId++;
const currentConnId = connectionId;

console.debug(`[pg] new connection ${currentConnId}`);

const conn = new PgClient({
database: 'db',
port: pgPort,
host: 'localhost',
user,
password,
ssl: false,
});
conn.on('error', (err) => {
console.log(err);
});
conn.on('end', () => {
console.debug(`[pg] end ${currentConnId}`);
});

await conn.connect();

return conn;
}

beforeAll(async () => {
db = await PostgresDBRunner.startContainer({});
birdbox = await getBirdbox(
'postgres',
{
...DEFAULT_CONFIG,
//
CUBESQL_LOG_LEVEL: 'trace',
//
CUBEJS_DB_TYPE: 'postgres',
CUBEJS_DB_HOST: db.getHost(),
CUBEJS_DB_PORT: `${db.getMappedPort(5432)}`,
CUBEJS_DB_NAME: 'test',
CUBEJS_DB_USER: 'test',
CUBEJS_DB_PASS: 'test',
//
CUBEJS_PG_SQL_PORT: `${pgPort}`,
},
{
schemaDir: 'postgresql/schema',
cubejsConfig: 'postgresql/single/sqlapi.js',
}
);
connection = await createPostgresClient('admin', 'admin_password');
});

afterAll(async () => {
await birdbox.stop();
await db.stop();
// await not working properly
await connection.end();
});

describe('Postgres (Auth)', () => {
test('Success Admin', async () => {
const conn = await createPostgresClient('admin', 'admin_password');

try {
const res = await conn.query('SELECT "user", "uid" FROM SecurityContextTest');
expect(res.rows).toEqual([{
user: 'admin',
uid: '1'
}]);
} finally {
await conn.end();
}
});

test('Error Admin Password', async () => {
try {
await createPostgresClient('admin', 'wrong_password');

throw new Error('Code must thrown auth error, something wrong...');
} catch (e) {
expect(e.message).toContain('password authentication failed for user "admin"');
}
});

test('Security Context (Admin -> Moderator) - allowed superuser', async () => {
const conn = await createPostgresClient('admin', 'admin_password');

try {
const res = await conn.query('SELECT "user", "uid" FROM SecurityContextTest WHERE __user = \'moderator\'');
expect(res.rows).toEqual([{
user: 'moderator',
uid: '2'
}]);
} finally {
await conn.end();
}
});

test('Security Context (Moderator -> Usr1) - allowed sqlCanChangeUser', async () => {
const conn = await createPostgresClient('moderator', 'moderator_password');

try {
const res = await conn.query('SELECT "user", "uid" FROM SecurityContextTest WHERE __user = \'usr1\'');
expect(res.rows).toEqual([{
user: 'usr1',
uid: '3'
}]);
} finally {
await conn.end();
}
});

test('Security Context (Moderator -> Usr2) - not allowed', async () => {
const conn = await createPostgresClient('moderator', 'moderator_password');

try {
await conn.query('SELECT "user", "uid" FROM SecurityContextTest WHERE __user = \'usr2\'');

throw new Error('Code must thrown auth error, something wrong...');
} catch (e) {
expect(e.message).toContain('You cannot change security context via __user from moderator to usr2, because it\'s not allowed');
} finally {
await conn.end();
}
});

test('Security Context (Usr1 -> Moderator) - not allowed', async () => {
const conn = await createPostgresClient('usr1', 'user1_password');

try {
await conn.query('SELECT "user", "uid" FROM SecurityContextTest WHERE __user = \'moderator\'');

throw new Error('Code must thrown auth error, something wrong...');
} catch (e) {
expect(e.message).toContain('You cannot change security context via __user from usr1 to moderator, because it\'s not allowed');
} finally {
await conn.end();
}
});
});

describe('Postgres (Data)', () => {
test('SELECT COUNT(*) as cn, "status" FROM Orders GROUP BY 2 ORDER BY cn DESC', async () => {
const res = await connection.query('SELECT COUNT(*) as cn, "status" FROM Orders GROUP BY 2 ORDER BY cn DESC');
expect(res.rows).toMatchSnapshot('sql_orders');
});
});
});

0 comments on commit 990802c

Please sign in to comment.