11# Copyright 2025 Canonical Ltd.
22# See LICENSE file for licensing details.
3+ # ruff: noqa: I001
34from unittest .mock import call , patch , sentinel
5+ from datetime import datetime , timezone , UTC
46
57import psycopg2
68import pytest
79from ops .testing import Harness
8- from psycopg2 .sql import SQL , Composed , Identifier , Literal
10+ from psycopg2 .sql import Composed , Identifier , Literal , SQL
11+
912from single_kernel_postgresql .abstract_charm import AbstractPostgreSQLCharm
10- from single_kernel_postgresql .config .literals import PEER , SYSTEM_USERS
13+ from single_kernel_postgresql .config .literals import (
14+ PEER ,
15+ POSTGRESQL_STORAGE_PERMISSIONS ,
16+ SNAP_USER ,
17+ SYSTEM_USERS ,
18+ )
1119from single_kernel_postgresql .utils .postgresql import (
12- ACCESS_GROUP_INTERNAL ,
1320 ACCESS_GROUPS ,
14- ROLE_DATABASES_OWNER ,
21+ ACCESS_GROUP_INTERNAL ,
1522 PostgreSQL ,
1623 PostgreSQLCreateDatabaseError ,
1724 PostgreSQLCreateUserError ,
1825 PostgreSQLDatabasesSetupError ,
1926 PostgreSQLGetLastArchivedWALError ,
2027 PostgreSQLUndefinedHostError ,
2128 PostgreSQLUndefinedPasswordError ,
29+ ROLE_DATABASES_OWNER ,
2230)
2331
2432
@@ -329,7 +337,14 @@ def test_set_up_database_with_temp_tablespace_and_missing_owner_role(harness):
329337 patch ("single_kernel_postgresql.utils.postgresql.PostgreSQL.create_user" ) as _create_user ,
330338 patch ("single_kernel_postgresql.utils.postgresql.change_owner" ) as _change_owner ,
331339 patch ("single_kernel_postgresql.utils.postgresql.os.chmod" ) as _chmod ,
340+ patch ("single_kernel_postgresql.utils.postgresql.os.stat" ) as _stat ,
341+ patch ("single_kernel_postgresql.utils.postgresql.pwd.getpwuid" ) as _getpwuid ,
332342 ):
343+ # Simulate a temp location owned by wrong user/permissions to trigger fixup
344+ stat_result = type ("stat_result" , (), {"st_uid" : 0 , "st_mode" : 0o755 })
345+ _stat .return_value = stat_result
346+ _getpwuid .return_value .pw_name = "root"
347+
333348 # First connection (non-context) for temp tablespace
334349 execute_direct = _connect_to_database .return_value .cursor .return_value .execute
335350 fetchone_direct = _connect_to_database .return_value .cursor .return_value .fetchone
@@ -346,8 +361,9 @@ def test_set_up_database_with_temp_tablespace_and_missing_owner_role(harness):
346361 _change_owner .assert_called_once_with ("/var/lib/postgresql/tmp" )
347362 _chmod .assert_called_once_with ("/var/lib/postgresql/tmp" , 0o700 )
348363
349- # Validate temp tablespace operations
364+ # Validate temp tablespace operations: check existence and create/grant when missing
350365 execute_direct .assert_has_calls ([
366+ call ("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';" ),
351367 call ("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';" ),
352368 call ("CREATE TABLESPACE temp LOCATION '/var/lib/postgresql/tmp';" ),
353369 call ("GRANT CREATE ON TABLESPACE temp TO public;" ),
@@ -371,6 +387,80 @@ def test_set_up_database_with_temp_tablespace_and_missing_owner_role(harness):
371387 execute_cm .assert_has_calls (expected , any_order = False )
372388
373389
390+ def test_set_up_database_owner_mismatch_triggers_rename_and_fix (harness ):
391+ with (
392+ patch (
393+ "single_kernel_postgresql.utils.postgresql.PostgreSQL._connect_to_database"
394+ ) as _connect_to_database ,
395+ patch ("single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_login_hook_function" ),
396+ patch (
397+ "single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_predefined_catalog_roles_function"
398+ ),
399+ patch ("single_kernel_postgresql.utils.postgresql.change_owner" ) as _change_owner ,
400+ patch ("single_kernel_postgresql.utils.postgresql.os.chmod" ) as _chmod ,
401+ patch ("single_kernel_postgresql.utils.postgresql.os.stat" ) as _stat ,
402+ patch ("single_kernel_postgresql.utils.postgresql.pwd.getpwuid" ) as _getpwuid ,
403+ patch ("single_kernel_postgresql.utils.postgresql.datetime" ) as _dt ,
404+ ):
405+ # Owner differs, permissions are correct
406+ stat_result = type (
407+ "stat_result" , (), {"st_uid" : 0 , "st_mode" : POSTGRESQL_STORAGE_PERMISSIONS }
408+ )
409+ _stat .return_value = stat_result
410+ _getpwuid .return_value .pw_name = "root"
411+
412+ # Mock datetime.now(timezone.utc) to a fixed timestamp
413+ _dt .now .return_value = datetime (2025 , 1 , 1 , 1 , 2 , 3 , tzinfo = UTC )
414+ _dt .timezone = timezone # ensure timezone.utc is available in the patch target
415+
416+ execute_direct = _connect_to_database .return_value .cursor .return_value .execute
417+ fetchone_direct = _connect_to_database .return_value .cursor .return_value .fetchone
418+ fetchone_direct .side_effect = [True , None ]
419+
420+ harness .charm .postgresql .set_up_database (temp_location = "/var/lib/postgresql/tmp" )
421+
422+ _change_owner .assert_called_once_with ("/var/lib/postgresql/tmp" )
423+ _chmod .assert_called_once_with ("/var/lib/postgresql/tmp" , POSTGRESQL_STORAGE_PERMISSIONS )
424+ execute_direct .assert_any_call ("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';" )
425+ execute_direct .assert_any_call ("ALTER TABLESPACE temp RENAME TO temp_20250101010203;" )
426+
427+
428+ def test_set_up_database_permissions_mismatch_triggers_rename_and_fix (harness ):
429+ with (
430+ patch (
431+ "single_kernel_postgresql.utils.postgresql.PostgreSQL._connect_to_database"
432+ ) as _connect_to_database ,
433+ patch ("single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_login_hook_function" ),
434+ patch (
435+ "single_kernel_postgresql.utils.postgresql.PostgreSQL.set_up_predefined_catalog_roles_function"
436+ ),
437+ patch ("single_kernel_postgresql.utils.postgresql.change_owner" ) as _change_owner ,
438+ patch ("single_kernel_postgresql.utils.postgresql.os.chmod" ) as _chmod ,
439+ patch ("single_kernel_postgresql.utils.postgresql.os.stat" ) as _stat ,
440+ patch ("single_kernel_postgresql.utils.postgresql.pwd.getpwuid" ) as _getpwuid ,
441+ patch ("single_kernel_postgresql.utils.postgresql.datetime" ) as _dt ,
442+ ):
443+ # Owner matches SNAP_USER, permissions differ
444+ stat_result = type ("stat_result" , (), {"st_uid" : 0 , "st_mode" : 0o755 })
445+ _stat .return_value = stat_result
446+ _getpwuid .return_value .pw_name = SNAP_USER
447+
448+ # Mock datetime.now(timezone.utc) to a fixed timestamp
449+ _dt .now .return_value = datetime (2025 , 1 , 1 , 1 , 2 , 3 , tzinfo = UTC )
450+ _dt .timezone = timezone
451+
452+ execute_direct = _connect_to_database .return_value .cursor .return_value .execute
453+ fetchone_direct = _connect_to_database .return_value .cursor .return_value .fetchone
454+ fetchone_direct .side_effect = [True , None ]
455+
456+ harness .charm .postgresql .set_up_database (temp_location = "/var/lib/postgresql/tmp" )
457+
458+ _change_owner .assert_called_once_with ("/var/lib/postgresql/tmp" )
459+ _chmod .assert_called_once_with ("/var/lib/postgresql/tmp" , POSTGRESQL_STORAGE_PERMISSIONS )
460+ execute_direct .assert_any_call ("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';" )
461+ execute_direct .assert_any_call ("ALTER TABLESPACE temp RENAME TO temp_20250101010203;" )
462+
463+
374464def test_set_up_database_no_temp_and_existing_owner_role (harness ):
375465 with (
376466 patch (
0 commit comments