diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index ae1c23cc..b178a03d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -127,6 +127,54 @@ jobs: - name: Run tests run: mix test + integration: + name: Run integration tests + runs-on: u22-arm-runner + needs: [deps] + + steps: + - uses: actions/checkout@v4 + - name: Setup Elixir + id: beam + uses: erlef/setup-beam@v1 + with: + otp-version: '25.3.2.7' + elixir-version: '1.14.5' + - uses: actions/setup-node@v4 + with: + node-version: 'latest' + - name: Set up Rust + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + - name: Cache Mix + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + - name: Cache native + uses: actions/cache@v4 + with: + path: | + _build/${{ env.MIX_ENV }}/lib/supavisor/native + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + key: ${{ runner.os }}-build-native-${{ hashFiles(format('{0}{1}', github.workspace, '/native/**/Cargo.lock')) }} + restore-keys: | + ${{ runner.os }}-build-native- + - name: Compile deps + run: mix deps.compile + - name: Compile + run: mix compile + - name: Set up Postgres + run: docker-compose -f ./docker-compose.db.yml up -d + - name: Start epmd + run: epmd -daemon + - name: Run tests + run: mix test --only integration --trace + dialyzer: name: Dialyze runs-on: u22-arm-runner diff --git a/config/test.exs b/config/test.exs index 0afa2fab..756a544f 100644 --- a/config/test.exs +++ b/config/test.exs @@ -46,6 +46,8 @@ config :supavisor, Supavisor.Vault, } ] +config :logger, level: :error + # Print only warnings and errors during test config :logger, :console, level: :error, diff --git a/test/integration/external_test.exs b/test/integration/external_test.exs new file mode 100644 index 00000000..357521e1 --- /dev/null +++ b/test/integration/external_test.exs @@ -0,0 +1,86 @@ +defmodule Integration.ExternalTest do + use ExUnit.Case, async: false + + @moduletag integration: true + + describe "Node" do + @describetag runtime: "node" + + setup ctx do + tool = get_tool("yarn") || raise "No Yarn" + + external_id = Enum.join([ctx.runtime, ctx.library, ctx.mode], "_") + + # Ensure that there are no leftovers + _ = Supavisor.Tenants.delete_tenant_by_external_id(external_id) + + _ = Supavisor.Repo.query("DROP DATABASE IF EXISTS #{external_id}") + assert {:ok, _} = Supavisor.Repo.query("CREATE DATABASE #{external_id}") + + assert {:ok, tenant} = + Supavisor.Tenants.create_tenant(%{ + default_parameter_status: %{}, + db_host: "localhost", + db_port: 6432, + db_database: external_id, + auth_query: "SELECT rolname, rolpassword FROM pg_authid WHERE rolname=$1;", + external_id: external_id, + users: [ + %{ + "pool_size" => 3, + "db_user" => "postgres", + "db_password" => "postgres", + "is_manager" => true, + "mode_type" => "session" + } + ] + }) + + {:ok, tool: tool, user: "postgres.#{external_id}", db: tenant.db_database} + end + + @tag library: "postgresjs", mode: "session" + test "Postgres.js session", ctx do + env = [ + {"NODE_OPTIONS", "--trace-uncaught"}, + {"PGMODE", ctx.mode}, + {"PGDATABASE", ctx.db}, + {"PGHOST", "localhost"}, + {"PGPORT", to_string(Application.fetch_env!(:supavisor, :proxy_port_session))}, + {"PGUSER", ctx.user}, + {"PGPASS", "postgres"} + ] + + # require IEx; IEx.pry + + assert {_, 0} = + System.cmd(ctx.tool, ~w[run test:postgres], + env: env, + cd: Path.join(__DIR__, "js") + ) + end + + @tag library: "postgresjs", mode: "transaction" + test "Postgres.js transaction", ctx do + env = [ + {"NODE_OPTIONS", "--trace-uncaught"}, + {"PGMODE", ctx.mode}, + {"PGDATABASE", ctx.db}, + {"PGHOST", "localhost"}, + {"PGPORT", to_string(Application.fetch_env!(:supavisor, :proxy_port_transaction))}, + {"PGUSER", ctx.user}, + {"PGPASS", "postgres"} + ] + + # require IEx; IEx.pry + + assert {_, 0} = + System.cmd(ctx.tool, ~w[run test:postgres], + env: env, + cd: Path.join(__DIR__, "js") + ) + end + end + + defp get_tool(name), do: System.find_executable(name) +end diff --git a/test/integration/js/postgres/bootstrap.js b/test/integration/js/postgres/bootstrap.js deleted file mode 100644 index 7090d7a1..00000000 --- a/test/integration/js/postgres/bootstrap.js +++ /dev/null @@ -1,36 +0,0 @@ -import { spawnSync } from 'child_process' - -//exec('dropdb', ['postgres_js_test']) - -//exec('psql', ['-c', 'alter system set ssl=on']) -//exec('psql', ['-c', 'drop user postgres_js_test']) -//exec('psql', ['-c', 'create user postgres_js_test']) -//exec('psql', ['-c', 'alter system set password_encryption=md5']) -//exec('psql', ['-c', 'select pg_reload_conf()']) -//exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) -//exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) -//exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) -//exec('psql', ['-c', 'select pg_reload_conf()']) -//exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) -//exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) -// -//exec('createdb', ['postgres_js_test']) -//exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) -//exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) - -exec('psql', ['-c', 'drop table test']) - -export function exec(cmd, args) { - const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) - if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) - throw stderr -} - -async function execAsync(cmd, args) { // eslint-disable-line - let stderr = '' - const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line - cp.stderr.on('data', x => stderr += x) - await new Promise(x => cp.on('exit', x)) - if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) - throw new Error(stderr) -} diff --git a/test/integration/js/postgres/index.js b/test/integration/js/postgres/index.js index 552a96f0..a8c64198 100644 --- a/test/integration/js/postgres/index.js +++ b/test/integration/js/postgres/index.js @@ -1,5 +1,3 @@ -import { exec } from './bootstrap.js' - import { t, nt, ot } from './test.js' // eslint-disable-line import net from 'net' import fs from 'fs' @@ -12,8 +10,8 @@ const rel = x => new URL(x, import.meta.url) const idle_timeout = 1 const login = { - user: 'postgres.sys', - pass: 'postgres' + user: process.env.PGUSER, + pass: process.env.PGPASS, } //const login_md5 = { @@ -27,8 +25,10 @@ const login = { //} const options = { - db: 'postgres', - prepare: true, + host: process.env.PGHOST, + port: process.env.PGPORT, + db: process.env.PGDATABASE, + prepare: (process.env.PGMODE != 'transaction'), user: login.user, pass: login.pass, idle_timeout, @@ -38,6 +38,8 @@ const options = { const sql = postgres(options) +await sql`DROP TABLE IF EXISTS test`; + //t('Connects with no options', async() => { // const sql = postgres({ max: 1 }) // @@ -240,19 +242,19 @@ t('Savepoint returns Result', async() => { return [1, result[0].x] }) -if (process.env.PGMODE != 'transaction') { -t('Prepared transaction', async() => { - await sql`create table test (a int)` +if (options.prepare) { + t('Prepared transaction', async() => { + await sql`create table test (a int)` - await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.prepare('tx1') - }) + await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.prepare('tx1') + }) - await sql`commit prepared 'tx1'` + await sql`commit prepared 'tx1'` - return ['1', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] -}) + return ['1', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] + }) } t('Transaction requests are executed implicitly', async() => { @@ -366,7 +368,7 @@ t('Throw syntax error', async() => t('Connect using uri', async() => [true, await new Promise((resolve, reject) => { - const sql = postgres('postgres://' + login.user + ':' + (login.pass || '') + '@localhost:5432/' + options.db, { + const sql = postgres(`postgres://${login.user}:${login.pass}@${options.host}:${options.port}/${options.db}`, { idle_timeout }) sql`select 1`.then(() => resolve(true), reject) @@ -750,44 +752,45 @@ t('simple query using simple() with multiple statements', async() => { ] }) -t('listen and notify', async() => { - const sql = postgres(options) - const channel = 'hello' - const result = await new Promise(async r => { - await sql.listen(channel, r) - sql.notify(channel, 'works') - }) +if (options.prepare) { + t('listen and notify', async() => { + const sql = postgres(options) + const channel = 'hello' + const result = await new Promise(async r => { + await sql.listen(channel, r) + sql.notify(channel, 'works') + }) - return [ - 'works', - result, - sql.end() - ] -}) + return [ + 'works', + result, + sql.end() + ] + }) -t('double listen', async() => { - const sql = postgres(options) - , channel = 'hello' + t('double listen', async() => { + const sql = postgres(options) + , channel = 'hello' - let count = 0 + let count = 0 - await new Promise((resolve, reject) => - sql.listen(channel, resolve) - .then(() => sql.notify(channel, 'world')) - .catch(reject) - ).then(() => count++) + await new Promise((resolve, reject) => + sql.listen(channel, resolve) + .then(() => sql.notify(channel, 'world')) + .catch(reject) + ).then(() => count++) - await new Promise((resolve, reject) => - sql.listen(channel, resolve) - .then(() => sql.notify(channel, 'world')) - .catch(reject) - ).then(() => count++) + await new Promise((resolve, reject) => + sql.listen(channel, resolve) + .then(() => sql.notify(channel, 'world')) + .catch(reject) + ).then(() => count++) - // for coverage - sql.listen('weee', () => { /* noop */ }).then(sql.end) + // for coverage + sql.listen('weee', () => { /* noop */ }).then(sql.end) - return [2, count] -}) + return [2, count] + }) // Reason: No LISTEN/NOTIFY //t('multiple listeners work after a reconnect', async() => { @@ -924,6 +927,7 @@ t('double listen', async() => { // // return ['1a2a1b', xs.join('')] //}) +} // Reason: We alter these parameters for PSQL, so it will not work as expected //t('responds with server parameters (application_name)', async() => @@ -1724,52 +1728,54 @@ t('Insert array in sql()', async() => { ] }) -t('Automatically creates prepared statements', async() => { - const result = await sql`select * from pg_prepared_statements` - return [true, result.some(x => x.name = result.statement.name)] -}) +if (options.prepare) { + t('Automatically creates prepared statements', async() => { + const result = await sql`select * from pg_prepared_statements` + return [true, result.some(x => x.name = result.statement.name)] + }) -t('no_prepare: true disables prepared statements (deprecated)', async() => { - const sql = postgres({ ...options, no_prepare: true }) - const result = await sql`select * from pg_prepared_statements` - return [false, result.some(x => x.name = result.statement.name)] -}) + t('no_prepare: true disables prepared statements (deprecated)', async() => { + const sql = postgres({ ...options, no_prepare: true }) + const result = await sql`select * from pg_prepared_statements` + return [false, result.some(x => x.name = result.statement.name)] + }) -t('prepare: false disables prepared statements', async() => { - const sql = postgres({ ...options, prepare: false }) - const result = await sql`select * from pg_prepared_statements` - return [false, result.some(x => x.name = result.statement.name)] -}) + t('prepare: false disables prepared statements', async() => { + const sql = postgres({ ...options, prepare: false }) + const result = await sql`select * from pg_prepared_statements` + return [false, result.some(x => x.name = result.statement.name)] + }) -t('prepare: true enables prepared statements', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql`select * from pg_prepared_statements` - return [true, result.some(x => x.name = result.statement.name)] -}) + t('prepare: true enables prepared statements', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql`select * from pg_prepared_statements` + return [true, result.some(x => x.name = result.statement.name)] + }) -t('prepares unsafe query when "prepare" option is true', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla'], { prepare: true }) - return [true, result.some(x => x.name = result.statement.name)] -}) + t('prepares unsafe query when "prepare" option is true', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla'], { prepare: true }) + return [true, result.some(x => x.name = result.statement.name)] + }) -t('does not prepare unsafe query by default', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla']) - return [false, result.some(x => x.name = result.statement.name)] -}) + t('does not prepare unsafe query by default', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla']) + return [false, result.some(x => x.name = result.statement.name)] + }) -t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, async() => { - const insert = () => sql`insert into test (name) values (${ '1' }) returning name` - await sql`create table test (name text)` - await insert() - await sql`alter table test alter column name type int using name::integer` - return [ - 1, - (await insert())[0].name, - await sql`drop table test` - ] -}) + t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, async() => { + const insert = () => sql`insert into test (name) values (${ '1' }) returning name` + await sql`create table test (name text)` + await insert() + await sql`alter table test alter column name type int using name::integer` + return [ + 1, + (await insert())[0].name, + await sql`drop table test` + ] + }) +} t('Throws correct error when retrying in transactions', async() => { await sql`create table test(x int)` @@ -2403,7 +2409,7 @@ t('Custom socket', {}, async() => { ...options, socket: () => new Promise((resolve, reject) => { const socket = new net.Socket() - socket.connect(5432) + socket.connect(options.port) socket.once('data', x => result = x[0]) socket.on('error', reject) socket.on('connect', () => resolve(socket)) diff --git a/test/test_helper.exs b/test/test_helper.exs index d7449a11..b4467cac 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -4,7 +4,10 @@ Cachex.start_link(name: Supavisor.Cache) ExUnit.start( capture_log: true, - exclude: [flaky: true] + exclude: [ + flaky: true, + integration: true + ] ) Ecto.Adapters.SQL.Sandbox.mode(Supavisor.Repo, :auto)