mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
## Problem `TYPE_CHECKING` is used inconsistently across Python tests. ## Summary of changes - Update `ruff`: 0.7.0 -> 0.11.2 - Enable TC (flake8-type-checking): https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc - (auto)fix all new issues
527 lines
18 KiB
Python
527 lines
18 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import requests
|
|
from fixtures.neon_fixtures import NeonEnv, logical_replication_sync
|
|
|
|
TEST_ROLE_NAMES = [
|
|
{"name": "neondb_owner"},
|
|
{"name": "role with spaces"},
|
|
{"name": "role with%20spaces "},
|
|
{"name": "role with whitespaces "},
|
|
{"name": "injective role with spaces'; SELECT pg_sleep(1000);"},
|
|
{"name": "role with #pound-sign and &ersands=true"},
|
|
{"name": "role with emoji 🌍"},
|
|
{"name": "role \";with ';injections $$ $x$ $ %I !/\\&#@"},
|
|
{"name": '"role in double quotes"'},
|
|
{"name": "'role in single quotes'"},
|
|
{"name": "role$"},
|
|
{"name": "role$$"},
|
|
{"name": "role$x$"},
|
|
]
|
|
|
|
TEST_DB_NAMES = [
|
|
{
|
|
"name": "neondb",
|
|
"owner": "neondb_owner",
|
|
},
|
|
{
|
|
"name": "db with spaces",
|
|
"owner": "role with spaces",
|
|
},
|
|
{
|
|
"name": "db with%20spaces ",
|
|
"owner": "role with%20spaces ",
|
|
},
|
|
{
|
|
"name": "db with whitespaces ",
|
|
"owner": "role with whitespaces ",
|
|
},
|
|
{
|
|
"name": "injective db with spaces'; SELECT pg_sleep(1000);",
|
|
"owner": "injective role with spaces'; SELECT pg_sleep(1000);",
|
|
},
|
|
{
|
|
"name": "db with #pound-sign and &ersands=true",
|
|
"owner": "role with #pound-sign and &ersands=true",
|
|
},
|
|
{
|
|
"name": "db with emoji 🌍",
|
|
"owner": "role with emoji 🌍",
|
|
},
|
|
{
|
|
"name": "db \";with ';injections $$ $x$ $ %I !/\\&#@",
|
|
"owner": "role \";with ';injections $$ $x$ $ %I !/\\&#@",
|
|
},
|
|
{
|
|
"name": '"db in double quotes"',
|
|
"owner": '"role in double quotes"',
|
|
},
|
|
{
|
|
"name": "'db in single quotes'",
|
|
"owner": "'role in single quotes'",
|
|
},
|
|
{
|
|
"name": "db name$",
|
|
"owner": "role$",
|
|
},
|
|
{
|
|
"name": "db name$$",
|
|
"owner": "role$$",
|
|
},
|
|
{
|
|
"name": "db name$x$",
|
|
"owner": "role$x$",
|
|
},
|
|
]
|
|
|
|
|
|
def test_compute_catalog(neon_simple_env: NeonEnv):
|
|
"""
|
|
Create a bunch of databases with tricky names and test that we can list them
|
|
and dump via API.
|
|
"""
|
|
env = neon_simple_env
|
|
|
|
endpoint = env.endpoints.create_start("main")
|
|
|
|
# Update the spec.json file to include new databases
|
|
# and reconfigure the endpoint to create some test databases.
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"roles": TEST_ROLE_NAMES,
|
|
"databases": TEST_DB_NAMES,
|
|
},
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
client = endpoint.http_client()
|
|
objects = client.dbs_and_roles()
|
|
|
|
# Assert that 'cloud_admin' role exists in the 'roles' list
|
|
assert any(role["name"] == "cloud_admin" for role in objects["roles"]), (
|
|
"The 'cloud_admin' role is missing"
|
|
)
|
|
|
|
# Assert that 'postgres' database exists in the 'databases' list
|
|
assert any(db["name"] == "postgres" for db in objects["databases"]), (
|
|
"The 'postgres' database is missing"
|
|
)
|
|
|
|
# Check other databases
|
|
for test_db in TEST_DB_NAMES:
|
|
db = next((db for db in objects["databases"] if db["name"] == test_db["name"]), None)
|
|
assert db is not None, f"The '{test_db['name']}' database is missing"
|
|
assert db["owner"] == test_db["owner"], (
|
|
f"The '{test_db['name']}' database has incorrect owner"
|
|
)
|
|
|
|
ddl = client.database_schema(database=test_db["name"])
|
|
|
|
# Check that it looks like a valid PostgreSQL dump
|
|
assert "-- PostgreSQL database dump complete" in ddl
|
|
|
|
# Check that it doesn't contain health_check and migration traces.
|
|
# They are only created in system `postgres` database, so by checking
|
|
# that we ensure that we dump right databases.
|
|
assert "health_check" not in ddl, f"The '{test_db['name']}' database contains health_check"
|
|
assert "migration" not in ddl, f"The '{test_db['name']}' database contains migrations data"
|
|
|
|
try:
|
|
client.database_schema(database="nonexistentdb")
|
|
raise AssertionError("Expected HTTPError was not raised")
|
|
except requests.exceptions.HTTPError as e:
|
|
assert e.response.status_code == 404, (
|
|
f"Expected 404 status code, but got {e.response.status_code}"
|
|
)
|
|
|
|
|
|
def test_compute_create_drop_dbs_and_roles(neon_simple_env: NeonEnv):
|
|
"""
|
|
Test that compute_ctl can create and work with databases and roles
|
|
with special characters (whitespaces, %, tabs, etc.) in the name.
|
|
"""
|
|
env = neon_simple_env
|
|
|
|
# Create and start endpoint so that neon_local put all the generated
|
|
# stuff into the spec.json file.
|
|
endpoint = env.endpoints.create_start("main")
|
|
|
|
# Update the spec.json file to include new databases
|
|
# and reconfigure the endpoint to apply the changes.
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"roles": TEST_ROLE_NAMES,
|
|
"databases": TEST_DB_NAMES,
|
|
},
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
for db in TEST_DB_NAMES:
|
|
# Check that database has a correct name in the system catalog
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT datname FROM pg_database WHERE datname = %s", (db["name"],))
|
|
catalog_db = cursor.fetchone()
|
|
assert catalog_db is not None
|
|
assert len(catalog_db) == 1
|
|
assert catalog_db[0] == db["name"]
|
|
|
|
# Check that we can connect to this database without any issues
|
|
with endpoint.cursor(dbname=db["name"]) as cursor:
|
|
cursor.execute("SELECT * FROM current_database()")
|
|
curr_db = cursor.fetchone()
|
|
assert curr_db is not None
|
|
assert len(curr_db) == 1
|
|
assert curr_db[0] == db["name"]
|
|
|
|
for role in TEST_ROLE_NAMES:
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT rolname FROM pg_roles WHERE rolname = %s", (role["name"],))
|
|
catalog_role = cursor.fetchone()
|
|
assert catalog_role is not None
|
|
assert catalog_role[0] == role["name"]
|
|
|
|
delta_operations = []
|
|
for db in TEST_DB_NAMES:
|
|
delta_operations.append({"action": "delete_db", "name": db["name"]})
|
|
for role in TEST_ROLE_NAMES:
|
|
delta_operations.append({"action": "delete_role", "name": role["name"]})
|
|
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"roles": [],
|
|
"databases": [],
|
|
},
|
|
"delta_operations": delta_operations,
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
for db in TEST_DB_NAMES:
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT datname FROM pg_database WHERE datname = %s", (db["name"],))
|
|
catalog_db = cursor.fetchone()
|
|
assert catalog_db is None
|
|
|
|
for role in TEST_ROLE_NAMES:
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT rolname FROM pg_roles WHERE rolname = %s", (role["name"],))
|
|
catalog_role = cursor.fetchone()
|
|
assert catalog_role is None
|
|
|
|
|
|
def test_dropdb_with_subscription(neon_simple_env: NeonEnv):
|
|
"""
|
|
Test that compute_ctl can drop a database that has a logical replication subscription.
|
|
"""
|
|
env = neon_simple_env
|
|
|
|
# Create and start endpoint so that neon_local put all the generated
|
|
# stuff into the spec.json file.
|
|
endpoint = env.endpoints.create_start("main")
|
|
|
|
SUB_DB_NAME = "';subscriber_db $$ $x$ $;"
|
|
PUB_DB_NAME = "publisher_db"
|
|
TEST_DB_NAMES = [
|
|
{
|
|
"name": "neondb",
|
|
"owner": "cloud_admin",
|
|
},
|
|
{
|
|
"name": SUB_DB_NAME,
|
|
"owner": "cloud_admin",
|
|
},
|
|
{
|
|
"name": PUB_DB_NAME,
|
|
"owner": "cloud_admin",
|
|
},
|
|
]
|
|
|
|
# Update the spec.json file to create the databases
|
|
# and reconfigure the endpoint to apply the changes.
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"databases": TEST_DB_NAMES,
|
|
},
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
# Connect to the PUB_DB_NAME and create a publication
|
|
with endpoint.cursor(dbname=PUB_DB_NAME) as cursor:
|
|
cursor.execute("CREATE PUBLICATION mypub FOR ALL TABLES")
|
|
cursor.execute("select pg_catalog.pg_create_logical_replication_slot('mysub', 'pgoutput');")
|
|
cursor.execute("CREATE TABLE t(a int)")
|
|
cursor.execute("INSERT INTO t VALUES (1)")
|
|
cursor.execute("CHECKPOINT")
|
|
|
|
# Connect to the SUB_DB_NAME and create a subscription
|
|
# Note that we need to create subscription with the following connstr:
|
|
connstr = endpoint.connstr(dbname=PUB_DB_NAME).replace("'", "''")
|
|
with endpoint.cursor(dbname=SUB_DB_NAME) as cursor:
|
|
cursor.execute("CREATE TABLE t(a int)")
|
|
cursor.execute(
|
|
f"CREATE SUBSCRIPTION mysub CONNECTION '{connstr}' PUBLICATION mypub WITH (create_slot = false) "
|
|
)
|
|
|
|
# Wait for the subscription to be active
|
|
logical_replication_sync(
|
|
endpoint,
|
|
endpoint,
|
|
"mysub",
|
|
sub_dbname=SUB_DB_NAME,
|
|
pub_dbname=PUB_DB_NAME,
|
|
)
|
|
|
|
# Check that replication is working
|
|
with endpoint.cursor(dbname=SUB_DB_NAME) as cursor:
|
|
cursor.execute("SELECT * FROM t")
|
|
rows = cursor.fetchall()
|
|
assert len(rows) == 1
|
|
assert rows[0][0] == 1
|
|
|
|
# Drop the SUB_DB_NAME from the list
|
|
TEST_DB_NAMES_NEW = [
|
|
{
|
|
"name": "neondb",
|
|
"owner": "cloud_admin",
|
|
},
|
|
{
|
|
"name": PUB_DB_NAME,
|
|
"owner": "cloud_admin",
|
|
},
|
|
]
|
|
# Update the spec.json file to drop the database
|
|
# and reconfigure the endpoint to apply the changes.
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"databases": TEST_DB_NAMES_NEW,
|
|
},
|
|
"delta_operations": [
|
|
{"action": "delete_db", "name": SUB_DB_NAME},
|
|
# also test the case when we try to delete a non-existent database
|
|
# shouldn't happen in normal operation,
|
|
# but can occur when failed operations are retried
|
|
{"action": "delete_db", "name": "nonexistent_db"},
|
|
],
|
|
}
|
|
)
|
|
|
|
logging.info(f"Reconfiguring the endpoint to drop the {SUB_DB_NAME} database")
|
|
endpoint.reconfigure()
|
|
|
|
# Check that the SUB_DB_NAME is dropped
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT datname FROM pg_database WHERE datname = %s", (SUB_DB_NAME,))
|
|
catalog_db = cursor.fetchone()
|
|
assert catalog_db is None
|
|
|
|
# Check that we can still connect to the PUB_DB_NAME
|
|
with endpoint.cursor(dbname=PUB_DB_NAME) as cursor:
|
|
cursor.execute("SELECT * FROM current_database()")
|
|
curr_db = cursor.fetchone()
|
|
assert curr_db is not None
|
|
assert len(curr_db) == 1
|
|
assert curr_db[0] == PUB_DB_NAME
|
|
|
|
|
|
def test_drop_role_with_table_privileges_from_neon_superuser(neon_simple_env: NeonEnv):
|
|
"""
|
|
Test that compute_ctl can drop a role even if it has some depending objects
|
|
like permissions in one of the databases that were granted by
|
|
neon_superuser.
|
|
|
|
Reproduction test for https://github.com/neondatabase/cloud/issues/13582
|
|
"""
|
|
env = neon_simple_env
|
|
TEST_DB_NAME = "db_with_permissions"
|
|
TEST_GRANTEE = "'); MALFORMED SQL $$ $x$ $/;5%$ %I"
|
|
|
|
endpoint = env.endpoints.create_start("main")
|
|
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"roles": [
|
|
{
|
|
# We need to create role via compute_ctl, because in this case it will receive
|
|
# additional grants equivalent to our real environment, so we can repro some
|
|
# issues.
|
|
"name": "neon",
|
|
# Some autocomplete-suggested hash, no specific meaning.
|
|
"encrypted_password": "SCRAM-SHA-256$4096:hBT22QjqpydQWqEulorfXA==$miBogcoj68JWYdsNB5PW1X6PjSLBEcNuctuhtGkb4PY=:hxk2gxkwxGo6P7GCtfpMlhA9zwHvPMsCz+NQf2HfvWk=",
|
|
"options": [],
|
|
},
|
|
],
|
|
"databases": [
|
|
{
|
|
"name": TEST_DB_NAME,
|
|
"owner": "neon",
|
|
},
|
|
],
|
|
},
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
with endpoint.cursor(dbname=TEST_DB_NAME) as cursor:
|
|
# Create table and view as `cloud_admin`. This is the case when, for example,
|
|
# PostGIS extensions creates tables in `public` schema.
|
|
cursor.execute("create table test_table (id int)")
|
|
cursor.execute("create view test_view as select * from test_table")
|
|
|
|
with endpoint.cursor(dbname=TEST_DB_NAME, user="neon") as cursor:
|
|
cursor.execute(f'create role "{TEST_GRANTEE}"')
|
|
# We (`compute_ctl`) make 'neon' the owner of schema 'public' in the owned database.
|
|
# Postgres has all sorts of permissions and grants that we may not handle well,
|
|
# but this is the shortest repro grant for the issue
|
|
# https://github.com/neondatabase/cloud/issues/13582
|
|
cursor.execute(f'grant select on all tables in schema public to "{TEST_GRANTEE}"')
|
|
|
|
# Check that role was created
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute(
|
|
"SELECT rolname FROM pg_roles WHERE rolname = %(role)s", {"role": TEST_GRANTEE}
|
|
)
|
|
role = cursor.fetchone()
|
|
assert role is not None
|
|
|
|
# Confirm that we actually have some permissions for 'readonly' role
|
|
# that may block our ability to drop the role.
|
|
with endpoint.cursor(dbname=TEST_DB_NAME) as cursor:
|
|
cursor.execute(
|
|
"select grantor from information_schema.role_table_grants where grantee = %(grantee)s",
|
|
{"grantee": TEST_GRANTEE},
|
|
)
|
|
res = cursor.fetchall()
|
|
assert len(res) == 2, f"Expected 2 table grants, got {len(res)}"
|
|
for row in res:
|
|
assert row[0] == "neon_superuser"
|
|
|
|
# Drop role via compute_ctl
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"delta_operations": [
|
|
{
|
|
"action": "delete_role",
|
|
"name": TEST_GRANTEE,
|
|
},
|
|
],
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
# Check that role is dropped
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute(
|
|
"SELECT rolname FROM pg_roles WHERE rolname = %(role)s", {"role": TEST_GRANTEE}
|
|
)
|
|
role = cursor.fetchone()
|
|
assert role is None
|
|
|
|
#
|
|
# Drop schema 'public' and check that we can still drop the role
|
|
#
|
|
with endpoint.cursor(dbname=TEST_DB_NAME) as cursor:
|
|
cursor.execute("create role readonly2")
|
|
cursor.execute("grant select on all tables in schema public to readonly2")
|
|
cursor.execute("drop schema public cascade")
|
|
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"delta_operations": [
|
|
{
|
|
"action": "delete_role",
|
|
"name": "readonly2",
|
|
},
|
|
],
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute("SELECT rolname FROM pg_roles WHERE rolname = 'readonly2'")
|
|
role = cursor.fetchone()
|
|
assert role is None
|
|
|
|
|
|
def test_drop_role_with_table_privileges_from_non_neon_superuser(neon_simple_env: NeonEnv):
|
|
"""
|
|
Test that compute_ctl can drop a role if the role has previously been
|
|
granted table privileges by a role other than neon_superuser.
|
|
"""
|
|
TEST_DB_NAME = "neondb"
|
|
TEST_GRANTOR = "; RAISE EXCEPTION 'SQL injection detected;"
|
|
TEST_GRANTEE = "'$$; RAISE EXCEPTION 'SQL injection detected;'"
|
|
|
|
env = neon_simple_env
|
|
|
|
endpoint = env.endpoints.create_start("main")
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"cluster": {
|
|
"roles": [
|
|
{
|
|
# We need to create role via compute_ctl, because in this case it will receive
|
|
# additional grants equivalent to our real environment, so we can repro some
|
|
# issues.
|
|
"name": TEST_GRANTOR,
|
|
# Some autocomplete-suggested hash, no specific meaning.
|
|
"encrypted_password": "SCRAM-SHA-256$4096:hBT22QjqpydQWqEulorfXA==$miBogcoj68JWYdsNB5PW1X6PjSLBEcNuctuhtGkb4PY=:hxk2gxkwxGo6P7GCtfpMlhA9zwHvPMsCz+NQf2HfvWk=",
|
|
"options": [],
|
|
},
|
|
],
|
|
"databases": [
|
|
{
|
|
"name": TEST_DB_NAME,
|
|
"owner": TEST_GRANTOR,
|
|
},
|
|
],
|
|
},
|
|
}
|
|
)
|
|
|
|
endpoint.reconfigure()
|
|
|
|
with endpoint.cursor(dbname=TEST_DB_NAME, user=TEST_GRANTOR) as cursor:
|
|
cursor.execute(f'CREATE USER "{TEST_GRANTEE}"')
|
|
cursor.execute("CREATE TABLE test_table(id bigint)")
|
|
cursor.execute(f'GRANT ALL ON TABLE test_table TO "{TEST_GRANTEE}"')
|
|
|
|
endpoint.respec_deep(
|
|
**{
|
|
"skip_pg_catalog_updates": False,
|
|
"delta_operations": [
|
|
{
|
|
"action": "delete_role",
|
|
"name": TEST_GRANTEE,
|
|
},
|
|
],
|
|
}
|
|
)
|
|
endpoint.reconfigure()
|
|
|
|
with endpoint.cursor() as cursor:
|
|
cursor.execute(
|
|
"SELECT rolname FROM pg_roles WHERE rolname = %(role)s", {"role": TEST_GRANTEE}
|
|
)
|
|
role = cursor.fetchone()
|
|
assert role is None
|