Files
RenewCMS/features/mysql_database_driver.feature
T

161 lines
7.3 KiB
Gherkin

Feature: Optional MySQL/MariaDB database driver
An operator who cannot use a local SQLite file runs RenewCMS against MySQL or
MariaDB by setting environment variables. SQLite remains the default and
behaves exactly as before.
Background:
Given the database drivers are registered by name in database.drivers
And the database is chosen by the DB_DRIVER environment variable
And repositories, use cases and handlers are unchanged
Scenario Outline: Driver selection
Given DB_DRIVER is "<value>"
When the server starts
Then the outcome is "<outcome>"
Examples:
| value | outcome |
| unset | the sqlite driver opens DB_FILE |
| sqlite | the sqlite driver opens DB_FILE |
| mysql | the mysql driver opens the DB_HOST connection |
| oracle | startup fails with: unsupported database driver "oracle" |
Scenario Outline: Missing MySQL variable
Given DB_DRIVER is "mysql"
And "<missing>" is not set
When the environment is validated
Then startup fails with "Environment variable <missing> is not set"
Examples:
| missing |
| DB_HOST |
| DB_USER |
| DB_PASSWORD |
| DB_NAME |
Scenario: SQLite does not require MySQL variables
Given DB_DRIVER is "sqlite"
And DB_HOST, DB_USER, DB_PASSWORD and DB_NAME are not set
When the environment is validated
Then validation succeeds
Scenario Outline: DSN built from variables
Given DB_HOST "<host>", DB_PORT "<port>", DB_USER "app", DB_NAME "cms", DB_TLS "<tls>"
When the MySQL DSN is built
Then the DSN targets "<host>:<expectedPort>" and database "cms"
And it contains parseTime=true, loc=UTC, charset=utf8mb4, clientFoundRows=true
And it contains tls="<tls>"
Examples:
| host | port | expectedPort | tls |
| db.local | | 3306 | false |
| db.local | 3307 | 3307 | true |
| mysql | 3306 | 3306 | skip-verify |
Scenario: Invalid TLS option is rejected
Given DB_DRIVER is "mysql" and DB_TLS is "maybe"
When the settings are read
Then startup fails naming DB_TLS and the accepted values
Scenario Outline: Fail fast when the server cannot be used
Given DB_DRIVER is "mysql" and "<fault>"
When the server starts
Then startup stops with "Unable to open the database"
And the message names the host and port
And the message does not contain the DB_PASSWORD value
Examples:
| fault |
| nothing listens on DB_PORT |
| the password is wrong |
| the database does not exist |
Scenario: Data directory with MySQL
Given DB_DRIVER is "mysql" and DATA_DIR is "/data"
Then update state, staging and backups live in "/data"
And DB_FILE is ignored
Scenario Outline: DataDir resolution
Given DB_FILE "<dbfile>" and DATA_DIR "<datadir>"
Then Settings.DataDir() is "<result>"
Examples:
| dbfile | datadir | result |
| ./data/renewcms.db | | ./data |
| /var/lib/cms/cms.db | | /var/lib/cms |
| ./data/renewcms.db | /srv/x | /srv/x |
Scenario: Connection pool
Given DB_MAX_OPEN_CONNS 4, DB_MAX_IDLE_CONNS 2 and DB_CONN_MAX_LIFETIME_SECONDS 60
When the MySQL connection is opened
Then the pool limits are 4 open, 2 idle and 60 seconds
Scenario: Migration on MySQL
Given an empty MySQL database
When database.Migrate runs
Then the tables articles, images, users, invites, api_keys and purge_settings exist
And the unique indexes on users and api_keys exist
And the first user lacking a role becomes "owner" and the others "admin"
Scenario: SQLite schema is untouched by the model changes
Given a SQLite file created by the models before this change, holding rows
When database.Migrate runs
Then the schema and every row are unchanged
Scenario Outline: Repository behaviour is the same on both drivers
Given the "<driver>" driver
Then the repository contract "<case>" holds
Examples:
| driver | case |
| sqlite | create user and read it back with its id and timestamps |
| mysql | create user and read it back with its id and timestamps |
| sqlite | username "Bob" and "bob" are distinct users |
| mysql | username "Bob" and "bob" are distinct users |
| sqlite | duplicate email returns an error and stores nothing |
| mysql | duplicate email returns an error and stores nothing |
| sqlite | unknown id returns domain ErrUserNotFound |
| mysql | unknown id returns domain ErrUserNotFound |
| sqlite | accept invite twice: the second returns ErrInviteNotFound |
| mysql | accept invite twice: the second returns ErrInviteNotFound |
| sqlite | accept invite with a taken username leaves the invite unaccepted |
| mysql | accept invite with a taken username leaves the invite unaccepted |
| sqlite | renew invite with unchanged values still succeeds |
| mysql | renew invite with unchanged values still succeeds |
| sqlite | duplicate image hashes are grouped and counted |
| mysql | duplicate image hashes are grouped and counted |
| sqlite | media search treats % and _ literally |
| mysql | media search treats % and _ literally |
| sqlite | article body of 1 MB round-trips |
| mysql | article body of 1 MB round-trips |
| sqlite | zero reset expiration round-trips as a zero time |
| mysql | zero reset expiration round-trips as a zero time |
| sqlite | purge settings save, then SaveLastRun with identical values succeeds |
| mysql | purge settings save, then SaveLastRun with identical values succeeds |
Scenario Outline: Backup before an update
Given the "<driver>" driver with a populated database
When the pre-update backup runs
Then a file "<pattern>" exists in DATA_DIR/backups
And only the 3 newest backups are kept
And a failure returns an error, so the update download fails instead of claiming a backup
Examples:
| driver | pattern |
| sqlite | renewcms-<time>.db |
| mysql | renewcms-<time>.sql |
Scenario: MySQL backup content
Given a MySQL database with 2 users and 1 article
When the pre-update backup runs
Then the dump contains the schema and the rows of every table
And it does not contain the DB_PASSWORD value
And replaying it into an empty database restores the same rows
Scenario: Secrets are never exposed
Given DB_DRIVER is "mysql"
Then no log line, error message or file in the repository contains the DB_PASSWORD value
Scenario: Static builds
Then the module builds with CGO_ENABLED=0 for the 6 task release targets