Add support for .pgpass to PostgreSQL (#3593)

Implements https://www.pivotaltracker.com/story/show/182582924
This commit is contained in:
Radosław Waśko 2022-07-21 15:32:37 +02:00 committed by GitHub
parent 7e2998bd27
commit 16fd038c1a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 628 additions and 40 deletions

View File

@ -164,6 +164,8 @@
- [Added `line_endings` and `comment_character` options to
`File_Format.Delimited`.][3581]
- [Fixed the case of various type names and library paths][3590]
- [Added support for parsing `.pgpass` file and `PG*` environment variables for
the Postgres connection][3593]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -260,6 +262,7 @@
[3581]: https://github.com/enso-org/enso/pull/3581
[3588]: https://github.com/enso-org/enso/pull/3588
[3590]: https://github.com/enso-org/enso/pull/3590
[3593]: https://github.com/enso-org/enso/pull/3593
#### Enso Compiler

View File

@ -50,6 +50,10 @@ nano_time = @Builtin_Method "System.nano_time"
os : Text
os = @Builtin_Method "System.os"
## Check if the operating system is UNIX.
is_unix : Boolean
is_unix = @Builtin_Method "System.is_unix"
## PRIVATE
Returns the default line separator for the platform that the program is
currently running on.

View File

@ -1,6 +1,6 @@
from Standard.Base import all
polyglot java import java.lang.System
polyglot java import org.enso.base.Environment_Utils
## ALIAS Read Environment
UNSTABLE
@ -18,4 +18,24 @@ polyglot java import java.lang.System
example_get = Environment.get "PATH"
get : Text -> Text | Nothing
get key = System.getenv key
get key = Environment_Utils.get_environment_variable key
## UNSTABLE
Returns a value of a specified environment variable or the provided default
value if such variable is not defined.
Arguments:
- key: The name of the environment variable to look up.
- default: The default fallback value.
> Example
Look up the value of the `FOOBAR` environment variable.
import Standard.Base.System.Environment
example_get_or_else = Environment.get_or_else "FOOBAR" "my default"
get_or_else : Text -> Text -> Text
get_or_else key ~default = case get key of
Nothing -> default
value -> value

View File

@ -2,6 +2,7 @@ from Standard.Base import all
import Standard.Base.System.File.Option
import Standard.Base.System.File.Existing_File_Behavior
import Standard.Base.System.File.File_Permissions
import Standard.Base.Error.Problem_Behavior
import Standard.Base.Data.Text.Matching_Mode
import Standard.Base.Data.Text.Text_Sub_Range
@ -368,9 +369,30 @@ type File
Builtin method that gets this file's last modified time.
Recommended to use `File.last_modified_time` instead which handles
potential exceptions.
last_modified_time_builtin : File -> ZonedDateTime
last_modified_time_builtin : ZonedDateTime
last_modified_time_builtin = @Builtin_Method "File.last_modified_time_builtin"
## Gets the POSIX permissions associated with the file.
> Example
Check if the file is readable by the user's group.
import Standard.Examples
example_permissions = Examples.csv.posix_permissions.group_read
posix_permissions : File_Permissions
posix_permissions =
File_Permissions.from_java_set self.posix_permissions_builtin
## PRIVATE
Builtin method that gets this file's POSIX permissions as a Java Set.
Recommended to use `File.posix_permissions` instead which handles
potential exceptions and converts an Enso representation of the
permissions.
posix_permissions_builtin : Set
posix_permissions_builtin = @Builtin_Method "File.posix_permissions_builtin"
## Checks whether the file exists and is a directory.
> Example

View File

@ -0,0 +1,104 @@
from Standard.Base import all
polyglot java import java.nio.file.attribute.PosixFilePermission
type Permission
## Permission for read access for a given entity.
type Read
## Permission for write access for a given entity.
type Write
## Permission for execute access for a given entity.
type Execute
type File_Permissions
## Access permissions for a file.
type File_Permissions (owner : Vector Permission) (group : Vector Permission) (others : Vector Permission)
## Converts the Enso atom to its Java enum counterpart.
to_java : Vector PosixFilePermission
to_java =
result = Vector.new_builder
if self.owner.contains Read then
result.append PosixFilePermission.OWNER_READ
if self.owner.contains Write then
result.append PosixFilePermission.OWNER_WRITE
if self.owner.contains Execute then
result.append PosixFilePermission.OWNER_EXECUTE
if self.group.contains Read then
result.append PosixFilePermission.GROUP_READ
if self.group.contains Write then
result.append PosixFilePermission.GROUP_WRITE
if self.group.contains Execute then
result.append PosixFilePermission.GROUP_EXECUTE
if self.others.contains Read then
result.append PosixFilePermission.OTHERS_READ
if self.others.contains Write then
result.append PosixFilePermission.OTHERS_WRITE
if self.others.contains Execute then
result.append PosixFilePermission.OTHERS_EXECUTE
result.to_vector
## Checks if the given file can be read by the owner.
owner_read : Boolean
owner_read = self.owner.contains Read
## Checks if the given file can be written by the owner.
owner_write : Boolean
owner_write = self.owner.contains Write
## Checks if the given file can be executed by the owner.
owner_execute : Boolean
owner_execute = self.owner.contains Execute
## Checks if the given file can be read by the group.
group_read : Boolean
group_read = self.group.contains Read
## Checks if the given file can be written by the group.
group_write : Boolean
group_write = self.group.contains Write
## Checks if the given file can be executed by the group.
group_execute : Boolean
group_execute = self.group.contains Execute
## Checks if the given file can be read by others.
others_read : Boolean
others_read = self.others.contains Read
## Checks if the given file can be written by others.
others_write : Boolean
others_write = self.others.contains Write
## Checks if the given file can be executed by others.
others_execute : Boolean
others_execute = self.others.contains Execute
## Converts a Java `Set` of Java `PosixFilePermission` to `File_Permissions`.
from_java_set java_set =
owner = Vector.new_builder
group = Vector.new_builder
others = Vector.new_builder
if java_set.contains PosixFilePermission.OWNER_READ then
owner.append Read
if java_set.contains PosixFilePermission.OWNER_WRITE then
owner.append Write
if java_set.contains PosixFilePermission.OWNER_EXECUTE then
owner.append Execute
if java_set.contains PosixFilePermission.GROUP_READ then
group.append Read
if java_set.contains PosixFilePermission.GROUP_WRITE then
group.append Write
if java_set.contains PosixFilePermission.GROUP_EXECUTE then
group.append Execute
if java_set.contains PosixFilePermission.OTHERS_READ then
others.append Read
if java_set.contains PosixFilePermission.OTHERS_WRITE then
others.append Write
if java_set.contains PosixFilePermission.OTHERS_EXECUTE then
others.append Execute
File_Permissions owner.to_vector group.to_vector others.to_vector

View File

@ -26,6 +26,10 @@ type Os
os : Os
os = from_text System.os
## Check if the currently running platform is a UNIX platform.
is_unix : Boolean
is_unix = System.is_unix
## PRIVATE
Create an Os object from text.

View File

@ -1,5 +1,7 @@
from Standard.Base import all
from Standard.Base.Data.Numbers import Parse_Error
import Standard.Database.Data.Dialect
import Standard.Database.Connection.Connection
from Standard.Database.Connection.Credentials as Credentials_Module import Credentials
@ -7,6 +9,7 @@ import Standard.Database.Connection.Connection_Options
import Standard.Database.Connection.SSL_Mode
from Standard.Database.Connection.SSL_Mode import all
import Standard.Database.Connection.Client_Certificate
import Standard.Database.Internal.Postgres.Pgpass
polyglot java import org.postgresql.Driver
@ -20,7 +23,7 @@ type Postgres
- credentials: The credentials to use for the connection (defaults to PGPass or No Authentication).
- use_ssl: Whether to use SSL (defaults to `Prefer`).
- client_cert: The client certificate to use or `Nothing` if not needed.
type Postgres (host:Text='localhost') (port:Integer=5432) (database:Text='') (credentials:(Credentials|Nothing)=Nothing) (use_ssl:SSL_Mode=Prefer) (client_cert:(Client_Certificate|Nothing)=Nothing)
type Postgres (host:Text=default_postgres_host) (port:Integer=default_postgres_port) (database:Text=default_postgres_database) (credentials:(Credentials|Nothing)=Nothing) (use_ssl:SSL_Mode=Prefer) (client_cert:(Client_Certificate|Nothing)=Nothing)
## Build the Connection resource.
@ -42,11 +45,22 @@ type Postgres
jdbc_properties : [Pair Text Text]
jdbc_properties =
credentials = case self.credentials of
Nothing -> Postgres.read_pgpass self.host self.port self.database
Nothing ->
env_user = Environment.get "PGUSER"
env_password = Environment.get "PGPASSWORD"
case Pair env_user env_password of
Pair Nothing Nothing ->
Pgpass.read self.host self.port self.database
Pair Nothing _ ->
Error.throw (Illegal_State_Error "PGPASSWORD is set, but PGUSER is not.")
Pair username Nothing ->
Pgpass.read self.host self.port self.database username
Pair username password ->
[Pair 'user' username, Pair 'password' password]
Credentials username password ->
[Pair 'user' username, Pair 'password' password]
ssl_properties = Postgres.ssl_mode_to_jdbc_properties self.use_ssl
ssl_properties = ssl_mode_to_jdbc_properties self.use_ssl
cert_properties = if self.client_cert.is_nothing then [] else
self.client_cert.properties
@ -57,31 +71,30 @@ type Postgres
dialect : Dialect
dialect = Dialect.postgres
## PRIVATE - static
Read the .pgpass file from the User's home directory and obtain username
and password. https://www.postgresql.org/docs/current/libpq-pgpass.html
## PRIVATE
Given an `SSL_Mode`, create the JDBC properties to secure a Postgres-based
connection.
ssl_mode_to_jdbc_properties : SSL_Mode -> [Pair Text Text]
ssl_mode_to_jdbc_properties use_ssl = case use_ssl of
Disable -> []
Prefer -> [Pair 'sslmode' 'prefer']
Require -> [Pair 'sslmode' 'require']
Verify_CA cert_file ->
if cert_file.is_nothing then [Pair 'sslmode' 'verify-ca'] else
[Pair 'sslmode' 'verify-ca', Pair 'sslrootcert' (File.new cert_file).absolute.path]
Full_Verification cert_file ->
if cert_file.is_nothing then [Pair 'sslmode' 'verify-full'] else
[Pair 'sslmode' 'verify-full', Pair 'sslrootcert' (File.new cert_file).absolute.path]
Arguments:
- host: The hostname of the database server.
- port: The port of the database server.
- database: The database to connect to.
read_pgpass : Text -> Integer -> Text -> [Pair Text Text]
read_pgpass _ _ _ =
## ToDo: Code not part of the design document.
## host port database
[]
## PRIVATE
default_postgres_host = Environment.get_or_else "PGHOST" "localhost"
## PRIVATE - static
Given an `SSL_Mode`, create the JDBC properties to secure a Postgres-based
connection.
ssl_mode_to_jdbc_properties : SSL_Mode -> [Pair Text Text]
ssl_mode_to_jdbc_properties use_ssl = case use_ssl of
Disable -> []
Prefer -> [Pair 'sslmode' 'prefer']
Require -> [Pair 'sslmode' 'require']
Verify_CA cert_file ->
if cert_file.is_nothing then [Pair 'sslmode' 'verify-ca'] else
[Pair 'sslmode' 'verify-ca', Pair 'sslrootcert' (File.new cert_file).absolute.path]
Full_Verification cert_file ->
if cert_file.is_nothing then [Pair 'sslmode' 'verify-full'] else
[Pair 'sslmode' 'verify-full', Pair 'sslrootcert' (File.new cert_file).absolute.path]
## PRIVATE
default_postgres_port =
hardcoded_port = 5432
case Environment.get "PGPORT" of
Nothing -> hardcoded_port
port -> Integer.parse port . catch Parse_Error (_->hardcoded_port)
## PRIVATE
default_postgres_database = Environment.get_or_else "PGDATABASE" ""

View File

@ -7,8 +7,7 @@ import Standard.Database.Connection.Connection_Options
import Standard.Database.Connection.SSL_Mode
from Standard.Database.Connection.SSL_Mode import all
import Standard.Database.Connection.Client_Certificate
import Standard.Database.Connection.Postgres
import Standard.Database.Internal.Postgres.Pgpass
polyglot java import com.amazon.redshift.jdbc.Driver
polyglot java import java.util.Properties
@ -53,7 +52,7 @@ type Redshift
jdbc_properties : [Pair Text Text]
jdbc_properties =
credentials = case self.credentials of
Nothing -> Postgres.Postgres.read_pgpass self.host self.port self.schema
Nothing -> Pgpass.read self.host self.port self.schema
AWS_Profile db_user profile ->
[Pair 'user' db_user] + (if profile == '' then [] else [Pair 'profile' profile])
AWS_Key db_user access_key secret_access_key ->

View File

@ -0,0 +1,126 @@
from Standard.Base import all
import Standard.Base.System.Platform
import Standard.Base.System.File.File_Permissions
polyglot java import java.lang.StringBuilder as Java_String_Builder
## PRIVATE
Read the .pgpass file from the User's home directory and obtain username
and password.
See https://www.postgresql.org/docs/current/libpq-pgpass.html
On Windows this file is expected to be located at
`%APPDATA%\postgresql\pgpass.conf`.
On Linux and macOS this file is expected to be located at `~/.pgpass` and
it is should be inaccessible by other users and the group - otherwise it
will be ignored. This can be achieved by running `chmod 0600 ~/.pgpass`.
If `PGPASSFILE` environment variable is set, the provided location is
used instead of the default one.
Arguments:
- host: The hostname of the database server.
- port: The port of the database server.
- database: The database to connect to.
read : Text -> Integer -> Text -> Text -> [Pair Text Text]
read host port database username=Nothing =
pgpass_file = locate
if pgpass_file.is_nothing || (verify pgpass_file . not) then [] else
entries = parse_file pgpass_file
found = entries.find entry->
entry.matches host port database username
case found.catch Nothing of
Nothing -> []
entry -> [Pair 'user' entry.username, Pair 'password' entry.password]
type Pgpass_Entry
## PRIVATE
type Pgpass_Entry host port database username password
## PRIVATE
matches : Text -> Text|Integer -> Text -> Text -> Boolean
matches host port database username=Nothing =
wildcard='*'
host_match = self.host==wildcard || self.host==host
port_match = self.port==wildcard ||
normalized_port = case port of
Integer -> port.to_text
Text -> port
self.port==normalized_port
database_match = self.database==wildcard || self.database==database
username_match = username==Nothing || self.username==wildcard || self.username==username
host_match && port_match && database_match && username_match
## PRIVATE
Determines the location of the .pgpass file to use.
locate = case Environment.get "PGPASSFILE" of
Nothing -> case Platform.os of
Platform.Windows -> case Environment.get "APPDATA" of
Nothing -> Nothing
appdata -> File.new appdata / "postgresql" / "pgpass.conf"
_ -> case Environment.get "HOME" of
Nothing -> Nothing
home -> File.new home / ".pgpass"
path -> File.new path
## PRIVATE
Checks if the given .pgpass file can be used.
The file can be used if it exists and has correct permissions on UNIX systems.
verify file = case Platform.os of
Platform.Windows -> file.exists
_ -> case file.exists of
False -> False
True ->
permissions = file.posix_permissions
can_others_access = permissions.group.not_empty || permissions.others.not_empty
can_others_access.not
## PRIVATE
parse_file file =
parse line =
if line.starts_with "#" || line.is_empty then Nothing else
elements = parse_line line
if elements.length != 5 then Nothing else
Pgpass_Entry (elements.at 0) (elements.at 1) (elements.at 2) (elements.at 3) (elements.at 4)
File.read_text file . lines . map parse . filter (x -> x.is_nothing.not)
## PRIVATE
parse_line line =
existing_entries = Vector.new_builder
current_entry = Java_String_Builder.new
next_entry =
existing_entries.append current_entry.toString
current_entry.setLength 0
characters = line.characters
go ix is_escape = case ix>=characters.length of
True ->
if is_escape then
# Handle the trailing escape character.
current_entry.append '\\'
next_entry
False ->
c = characters.at ix
case c=='\\' of
True ->
if is_escape then
current_entry.append '\\'
@Tail_Call go (ix+1) is_escape.not
False -> case c==':' of
True ->
case is_escape of
True -> current_entry.append ':'
False -> next_entry
@Tail_Call go (ix+1) False
False ->
if is_escape then
# Handle escape character followed by other characters.
current_entry.append '\\'
# Any other character is just appended and escape is reset.
current_entry.append c
@Tail_Call go (ix+1) False
go 0 False
existing_entries.to_vector

View File

@ -0,0 +1,15 @@
from Standard.Base import all
polyglot java import org.enso.base.Environment_Utils
## UNSTABLE
ADVANCED
Runs a given action with an environment variable modified to a given value.
The environment variable is restored to its original value after the action.
The environment variable override is only visible to the Enso
`Environment.get` method, the environment as seen from a direct
`System.getenv` Java call remains unchanged.
unsafe_with_environment_override : Text -> Text -> Any -> Any
unsafe_with_environment_override key value ~action =
Environment_Utils.with_environment_variable_override key value (_->action)

View File

@ -19,8 +19,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.CopyOption;
import java.nio.file.OpenOption;
import java.time.ZonedDateTime;
import java.nio.file.attribute.PosixFilePermission;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Set;
/**
* A wrapper for {@link TruffleFile} objects exposed to the language. For methods documentation
@ -80,6 +82,13 @@ public class EnsoFile implements TruffleObject {
return ZonedDateTime.ofInstant(truffleFile.getLastModifiedTime().toInstant(), ZoneOffset.UTC);
}
@Builtin.Method(name = "posix_permissions_builtin")
@Builtin.WrapException(from = IOException.class, to = PolyglotError.class, propagate = true)
@Builtin.ReturningGuestObject
public Set<PosixFilePermission> getPosixPermissions() throws IOException {
return truffleFile.getPosixPermissions();
}
@Builtin.Method(name = "parent")
public EnsoFile getParent() {
return new EnsoFile(this.truffleFile.getParent());

View File

@ -30,6 +30,12 @@ public class System {
return UNKNOWN;
}
@Builtin.Method(description = "Check if the operating system is UNIX.")
@CompilerDirectives.TruffleBoundary
public static Boolean is_unix() {
return SystemUtils.IS_OS_UNIX;
}
@Builtin.Method(description = "Gets the nanosecond resolution system time.")
@CompilerDirectives.TruffleBoundary
public static long nanoTime() {

View File

@ -0,0 +1,44 @@
package org.enso.base;
import java.util.HashMap;
import java.util.function.Function;
public class Environment_Utils {
/** Gets the environment variable, including any overrides. */
public static String get_environment_variable(String name) {
String override = overrides.get(name);
if (override != null) {
return override;
} else {
return System.getenv(name);
}
}
/**
* Calls `action` with the provided environment variable.
*
* <p>The override is not persisted (its only visible from within the action called by this
* method) and it is only visible by the Enso `Environment.get` method (backed by {@code
* get_environment_variable}).
*
* <p>This is an internal function that should be used very carefully and only for testing.
*/
public static <T> T with_environment_variable_override(
String name, String value, Function<Object, T> action) {
String oldValue = overrides.put(name, value);
boolean was_set = oldValue != null;
try {
// Giving 0 here as an argument, as using null would lead to incorrect behaviour, due to some
// weird Truffle peculiarity.
return action.apply(0);
} finally {
if (was_set) {
overrides.put(name, oldValue);
} else {
overrides.remove(name);
}
}
}
private static final HashMap<String, String> overrides = new HashMap<>();
}

View File

@ -0,0 +1,24 @@
#hostname:port:database:username:password
localhost:5432:postgres:postgres:postgres
192.168.4.0:1234:foo:bar:baz
#some interesting comment
host with \: semicolons in it? what?:*:*:*:well yes, that is possible, the \:password\: can contain those as well
\::\::\::\::\:
::wrong amount of entries is skipped
:::::::::::
you can escape an escape too\: see \\\\:*:*:*:yes it is possible
other escapes like \n or \? :*:*:*:are just parsed as-is
a trailing escape character:*:*:*:is treated as a regular slash\
passwords should preserve leading space:*:*:*: pass
\\\::*:*:*:\\\:
#example.com:443:foo:bar:baz
\:\:1:*:database_name:user_that_has_no_password:
*:*:*:*:fallback_password
order_matters:1234:this:will_still_match_the_fallback_password:not_this_one

View File

@ -1,10 +1,12 @@
from Standard.Base import all
import Standard.Base.System.Environment
import Standard.Base.Runtime.Ref
import Standard.Base.System.Platform
import Standard.Base.System.Process
from Standard.Base.System.Process.Exit_Code import Exit_Success
from Standard.Database import all
from Standard.Database.Connection.Connection import Sql_Error
import Standard.Test
import Standard.Table as Materialized_Table
import project.Database.Common_Spec
import project.Database.Helpers.Name_Generator
@ -12,6 +14,10 @@ import project.Common_Table_Spec
import project.Aggregate_Spec
from Standard.Table.Data.Aggregate_Column import all
from Standard.Database.Data.Sql import Sql_Type
from Standard.Database.Internal.Postgres.Pgpass import Pgpass_Entry
import Standard.Test
import Standard.Test.Test_Environment
postgres_specific_spec connection pending =
Test.group "[PostgreSQL] Info" pending=pending <|
@ -112,7 +118,7 @@ run_tests connection pending=Nothing =
clean_tables tables.to_vector
spec =
table_spec =
db_name = Environment.get "ENSO_DATABASE_TEST_DB_NAME"
db_host_port = (Environment.get "ENSO_DATABASE_TEST_HOST").if_nothing "localhost" . split ':'
db_user = Environment.get "ENSO_DATABASE_TEST_DB_USER"
@ -128,4 +134,133 @@ spec =
connection = Database.connect (Postgres (db_host_port.at 0) db_port db_name (Credentials db_user db_password))
run_tests connection
pgpass_file = enso_project.data / "pgpass.conf"
pgpass_spec = Test.group "[PostgreSQL] .pgpass" <|
make_pair username password =
[Pair "user" username, Pair "password" password]
Test.specify "should correctly parse the file, including escapes, blank lines and comments" <|
result = Pgpass.parse_file pgpass_file
result.length . should_equal 12
e1 = Pgpass_Entry "localhost" "5432" "postgres" "postgres" "postgres"
e2 = Pgpass_Entry "192.168.4.0" "1234" "foo" "bar" "baz"
e3 = Pgpass_Entry "host with : semicolons in it? what?" "*" "*" "*" "well yes, that is possible, the :password: can contain those as well"
e4 = Pgpass_Entry ":" ":" ":" ":" ":"
e5 = Pgpass_Entry "you can escape an escape too: see \\" "*" "*" "*" "yes it is possible"
e6 = Pgpass_Entry "other escapes like \n or \? " "*" "*" "*" "are just parsed as-is"
e7 = Pgpass_Entry "a trailing escape character" "*" "*" "*" "is treated as a regular slash\"
e8 = Pgpass_Entry "passwords should preserve leading space" "*" "*" "*" " pass"
e9 = Pgpass_Entry "\:" "*" "*" "*" "\:"
e10 = Pgpass_Entry "::1" "*" "database_name" "user_that_has_no_password" ""
e11 = Pgpass_Entry "*" "*" "*" "*" "fallback_password"
e12 = Pgpass_Entry "order_matters" "1234" "this" "will_still_match_the_fallback_password" "not_this_one"
entries = [e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12]
result.should_equal entries
if Platform.is_unix then
Test.specify "should only accept the .pgpass file if it has correct permissions" <|
Process.run "chmod" ["0777", pgpass_file.absolute.path] . should_equal Exit_Success
Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <|
Pgpass.verify pgpass_file . should_equal False
Pgpass.read "passwords should preserve leading space" "1" "some database name that is really : weird" . should_equal []
Process.run "chmod" ["0400", pgpass_file.absolute.path] . should_equal Exit_Success
Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <|
Pgpass.verify pgpass_file . should_equal True
Pgpass.read "passwords should preserve leading space" "1" "some database name that is really : weird" . should_equal (make_pair "*" " pass")
Test.specify "should correctly match wildcards and use the first matching entry" <|
Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <|
Pgpass.read "localhost" 5432 "postgres" . should_equal (make_pair "postgres" "postgres")
Pgpass.read "192.168.4.0" "1234" "foo" . should_equal (make_pair "bar" "baz")
Pgpass.read "" "" "" . should_equal (make_pair "*" "fallback_password")
Pgpass.read "blah" "5324" "blah" . should_equal (make_pair "*" "fallback_password")
Pgpass.read "::1" "55999" "database_name" . should_equal (make_pair "user_that_has_no_password" "")
Pgpass.read "order_matters" "1234" "this" . should_equal (make_pair "*" "fallback_password")
Pgpass.read "\:" "1234" "blah" . should_equal (make_pair "*" "\:")
Pgpass.read ":" ":" ":" . should_equal (make_pair ":" ":")
connection_setup_spec = Test.group "[PostgreSQL] Connection setup" <|
Test.specify "should use environment variables as host, port and database defaults and fall back to hardcoded defaults" <|
c1 = Postgres "example.com" 12345 "my_db"
c2 = Postgres
c3 = Test_Environment.unsafe_with_environment_override "PGHOST" "192.168.0.1" <|
Test_Environment.unsafe_with_environment_override "PGPORT" "1000" <|
Test_Environment.unsafe_with_environment_override "PGDATABASE" "ensoDB" <|
Postgres
c1.host . should_equal "example.com"
c1.port . should_equal 12345
c1.database . should_equal "my_db"
c1.jdbc_url . should_equal "jdbc:postgresql://example.com:12345/my_db"
c2.host . should_equal "localhost"
c2.port . should_equal 5432
c2.database . should_equal ""
c2.jdbc_url . should_equal "jdbc:postgresql://localhost:5432"
c3.host . should_equal "192.168.0.1"
c3.port . should_equal 1000
c3.database . should_equal "ensoDB"
c3.jdbc_url . should_equal "jdbc:postgresql://192.168.0.1:1000/ensoDB"
## Currently we require the port to be numeric. When we support
Unix-sockets, we may lift that restriction.
c4 = Test_Environment.unsafe_with_environment_override "PGPORT" "foobar" <|
Postgres
c4.host . should_equal "localhost"
c4.port . should_equal 5432
c4.database . should_equal ""
c4.jdbc_url . should_equal "jdbc:postgresql://localhost:5432"
add_ssl props = props+[Pair 'sslmode' 'prefer']
Test.specify "should use the given credentials" <|
c = Postgres credentials=(Credentials "myuser" "mypass")
c.jdbc_url . should_equal "jdbc:postgresql://localhost:5432"
c.jdbc_properties . should_equal <| add_ssl [Pair "user" "myuser", Pair "password" "mypass"]
Test.specify "should fallback to environment variables and fill-out missing information based on the PGPASS file (if available)" <|
c1 = Postgres
c1.jdbc_url . should_equal "jdbc:postgresql://localhost:5432"
c1.jdbc_properties . should_equal <| add_ssl []
Test_Environment.unsafe_with_environment_override "PGPASSWORD" "somepassword" <|
c1.jdbc_properties . should_fail_with Illegal_State_Error
c1.jdbc_properties.catch.message . should_equal "PGPASSWORD is set, but PGUSER is not."
Test_Environment.unsafe_with_environment_override "PGUSER" "someuser" <|
c1.jdbc_properties . should_equal <| add_ssl [Pair "user" "someuser", Pair "password" "somepassword"]
c2 = Postgres "192.168.4.0" 1234 "foo"
c3 = Postgres "::1" 55999 "database_name"
c4 = Postgres "::1" 55999 "otherDB"
c2.jdbc_properties . should_equal <| add_ssl []
c3.jdbc_properties . should_equal <| add_ssl []
c4.jdbc_properties . should_equal <| add_ssl []
Test_Environment.unsafe_with_environment_override "PGPASSFILE" pgpass_file.absolute.path <|
c2.jdbc_properties . should_equal <| add_ssl [Pair "user" "bar", Pair "password" "baz"]
c3.jdbc_properties . should_equal <| add_ssl [Pair "user" "user_that_has_no_password", Pair "password" ""]
c4.jdbc_properties . should_equal <| add_ssl [Pair "user" "*", Pair "password" "fallback_password"]
Test_Environment.unsafe_with_environment_override "PGUSER" "bar" <|
c2.jdbc_properties . should_equal <| add_ssl [Pair "user" "bar", Pair "password" "baz"]
[c3, c4].each c->
c.jdbc_properties . should_equal <|
add_ssl [Pair "user" "*", Pair "password" "fallback_password"]
Test_Environment.unsafe_with_environment_override "PGUSER" "other user" <|
[c2, c3, c4].each c->
c.jdbc_properties . should_equal <|
add_ssl [Pair "user" "*", Pair "password" "fallback_password"]
Test_Environment.unsafe_with_environment_override "PGPASSWORD" "other password" <|
[c2, c3, c4].each c->
c.jdbc_properties . should_equal <| add_ssl [Pair "user" "other user", Pair "password" "other password"]
spec =
table_spec
pgpass_spec
connection_setup_spec
main = Test.Suite.run_main spec

View File

@ -57,6 +57,7 @@ import project.Resource.Bracket_Spec
import project.Runtime.Stack_Traces_Spec
import project.Runtime.Lazy_Generator_Spec
import project.System.Environment_Spec
import project.System.File_Spec
import project.System.Process_Spec
import project.System.Reporting_Stream_Decoder_Spec
@ -72,6 +73,7 @@ main = Test.Suite.run_main <|
Conversion_Spec.spec
Deep_Export_Spec.spec
Error_Spec.spec
Environment_Spec.spec
File_Spec.spec
Reporting_Stream_Decoder_Spec.spec
Reporting_Stream_Encoder_Spec.spec

View File

@ -0,0 +1,35 @@
from Standard.Base import all
import Standard.Base.System.Environment
import Standard.Test
import Standard.Test.Test_Environment
spec = Test.group "Environment" <|
Test.specify "should allow to internally override environment variables for testing purposes" <|
old = Environment.get "foobar"
result_0 = Test_Environment.unsafe_with_environment_override "foobar" "value1" 23
result_0 . should_equal 23
result_1 = Test_Environment.unsafe_with_environment_override "foobar" "value1" <|
Environment.get "foobar" . should_equal "value1"
42
result_2 = Test_Environment.unsafe_with_environment_override "foobar" "other interesting value" <|
Environment.get "foobar"
result_1 . should_equal 42
result_2 . should_equal "other interesting value"
Environment.get "foobar" . should_equal old
result_3 = Test_Environment.unsafe_with_environment_override "foo" "1" <|
Environment.get "foo" . should_equal "1"
x = Test_Environment.unsafe_with_environment_override "foo" "2" <|
Environment.get "foo" . should_equal "2"
Test_Environment.unsafe_with_environment_override "bar" "3" <|
Test_Environment.unsafe_with_environment_override "baz" "4" <|
[Environment.get "foo", Environment.get "bar", Environment.get "baz"]
Environment.get "foo" . should_equal "1"
x
result_3 . should_equal ["2", "3", "4"]
main = Test.Suite.run_main spec

View File

@ -4,6 +4,10 @@ from Standard.Base.Data.Text.Encoding as Encoding_Module import Encoding, Encodi
import Standard.Base.System.File.Existing_File_Behavior
from Standard.Base.System.File import File_Already_Exists_Error
from Standard.Base.Error.Problem_Behavior import all
import Standard.Base.System.Platform
import Standard.Base.System.Process
from Standard.Base.System.File.File_Permissions as File_Permissions_Module import all
from Standard.Base.System.Process.Exit_Code import Exit_Success
import Standard.Test
import Standard.Test.Problems
@ -62,6 +66,23 @@ spec =
file = File.new "does_not_exist.txt"
file.delete . should_fail_with File.File_Not_Found
if Platform.is_unix then
Test.specify "should allow to check file permissions" <|
f = enso_project.data / "transient" / "permissions.txt"
f.delete_if_exists
"foobar".write f
Process.run "chmod" ["0777", f.absolute.path] . should_equal Exit_Success
rwx = [Read, Write, Execute]
f.posix_permissions . should_equal <|
File_Permissions rwx rwx rwx
Process.run "chmod" ["0421", f.absolute.path] . should_equal Exit_Success
f.posix_permissions . should_equal <|
File_Permissions [Read] [Write] [Execute]
f.delete
Test.group "read_bytes" <|
Test.specify "should allow reading a file to byte vector" <|
contents = sample_file.read_bytes

View File

@ -7,3 +7,5 @@ spec = Test.group "System" <|
Test.specify "should provide nanosecond timer" <|
result = System.nano_time
(result > 0).should_equal True
main = Test.Suite.run_main spec