Skip to content

Commit

Permalink
initial changes to support unit testing via phpunit (#911)
Browse files Browse the repository at this point in the history
There is a simple test for retrieving accounts via account id using a mock mysql object

closes #910
  • Loading branch information
XaeroDegreaz authored Nov 12, 2020
1 parent 34c4d98 commit 0710abf
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 57 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true

[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/.buildpath
/.project
/.idea
*.iml
/nbproject
/composer.lock

Expand All @@ -23,7 +24,7 @@
/htdocs/upload

# DKIM private keys
/opendkim/*.private
/opendkim

# Docker data volumes
/vol_*
Expand All @@ -32,3 +33,5 @@

/newsletters
/vendor
.phpunit*
out/
13 changes: 13 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
services:
- docker
language: php
php:
- '7.4'
Expand All @@ -6,3 +8,14 @@ git:
script:
- tests/phplint.sh
- tests/strict_types.sh
- composer install
- mv .env.sample .env
- mv config/config.specific.sample.php config/config.specific.php
- mv config/SmrMySqlSecrets.sample.inc config/SmrMySqlSecrets.inc
- mv config/discord/config.specific.sample.php config/discord/config.specific.php
- mv config/irc/config.specific.sample.php config/irc/config.specific.php
- mv config/npc/config.specific.sample.php config/npc/config.specific.php
- docker-compose up -d mysql-integration-test
- sleep 10
- docker-compose run --rm flyway-integration-test
- vendor/bin/phpunit test
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Make sure the following software is installed:
* docker (version 18.06.0+)
* docker-compose (version 1.22.0+)

To run unit tests on your machine:
* Composer (2.0.5+)

## Setup
First, you will need to clone this repository. Then inside the clone, you
will need to create installation-specific copies of the following files:
Expand Down Expand Up @@ -175,3 +178,18 @@ For any page which takes input through POST or GET (or other forms?) they should
## Abstract vs normal classes
This initially started out to be used in the "standard" way for NPCs but that idea has since been discarded.
Now all core/shared "Default" code should be in the abstract version, with the normal class child implementing game type specific functionality/overrides, for instance "lib/Semi Wars/SmrAccount" which is used to make every account appear to be a "vet" account when playing semi wars.

## Unit testing
SMR uses [PHPUnit](https://phpunit.de/) to run unit tests.
### Setup
1. Ensure the `mysqli` extension is enabled in your local PHP installation's `php.ini`, as this is a hard dependency.
1. From the root directory run `composer install` to install dependencies, and configure the development auto loader.
1. In some cases, you may have to manually run `docker-compose up -d mysql-integration-test` in order to have the integration test MySQL Docker container ready to receive connections for the `flyway` migration that happens during integration tests. This only needs to be done once, but if you ever stop this container you should repeat this step before running any integration tests.
1. Run `vendor/bin/phpunit test` to execute the full suite of tests.
1. Add new tests as needed in the `/test` directory.

* For information on running one-off tests inside your IDE, please refer to your IDE vendor's documentation on how to run PHPUnit tests.
* If you're running one-off unit tests that don't need database interaction, you don't need to start up the `mysql-integration-test` Docker service.

### Integration testing
1. To create an integration test that uses the database, your test should extend `SmrTest\BaseIntegrationSpec`. This will ensure that the `flyway` migration happens before your test suite initializes, and cleans up any records written between tests.
36 changes: 22 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
{
"name": "smr/smr",
"description": "SMR",
"license": "AGPL-3.0",
"require": {
"abraham/twitteroauth": "^1.0",
"ext-curl": "*",
"ext-json": "*",
"ext-mysqli": "*",
"facebook/graph-sdk": "^5.5.0",
"google/recaptcha": "^1.1",
"php": "^7.4",
"phpmailer/phpmailer": "^6.1",
"vanilla/nbbc": "^2.0"
}
"name": "smr/smr",
"description": "SMR",
"license": "AGPL-3.0",
"require": {
"abraham/twitteroauth": "^1.0",
"ext-curl": "*",
"ext-json": "*",
"ext-mysqli": "*",
"facebook/graph-sdk": "^5.5.0",
"google/recaptcha": "^1.1",
"php": "^7.4",
"phpmailer/phpmailer": "^6.1",
"vanilla/nbbc": "^2.0"
},
"autoload-dev": {
"psr-0": {
"SmrTest\\": "test/"
}
},
"require-dev": {
"phpunit/phpunit": "9.4.2"
}
}
49 changes: 31 additions & 18 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ x-smr-common: &smr-common
- ./vol_upload:/smr/htdocs/upload
- ./config:/smr/config:ro

x-mysql-common: &mysql-common
image: mysql:8.0
container_name: ${MYSQL_HOST}
networks:
- backend
# By using the default image, we must expose the secrets in
# the runtime environment (because we can't specify build args).
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_USER: smr
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: smr_live
# The mysql:5.7+ docker default sql mode uses STRICT_TRANS_TABLES,
# which is incompatible with the way the SMR database is used.
# Therefore, we override CMD to omit this sql mode.
command: ["mysqld", "--sql-mode=NO_ENGINE_SUBSTITUTION",
"--character-set-server=utf8",
"--collation-server=utf8_general_ci"]

services:
smr:
<< : *smr-common
Expand All @@ -60,7 +79,7 @@ services:
volumes:
- ./opendkim:/etc/opendkim/keys/smrealms.de

flyway:
flyway: &flyway-common
image: boxfuse/flyway:latest-alpine
command: -url=jdbc:mysql://${MYSQL_HOST}/smr_live?allowPublicKeyRetrieval=true&useSSL=false -user=smr -password=${MYSQL_PASSWORD} migrate
networks:
Expand All @@ -70,26 +89,20 @@ services:
volumes:
- ./db/patches:/flyway/sql:ro

flyway-integration-test:
<< : *flyway-common
depends_on:
- mysql-integration-test

mysql:
image: mysql:8.0
container_name: ${MYSQL_HOST}
networks:
- backend
# By using the default image, we must expose the secrets in
# the runtime environment (because we can't specify build args).
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_USER: smr
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_DATABASE: smr_live
<< : *mysql-common
volumes:
- ./vol_db:/var/lib/mysql
# The mysql:5.7+ docker default sql mode uses STRICT_TRANS_TABLES,
# which is incompatible with the way the SMR database is used.
# Therefore, we override CMD to omit this sql mode.
command: ["mysqld", "--sql-mode=NO_ENGINE_SUBSTITUTION",
"--character-set-server=utf8",
"--collation-server=utf8_general_ci"]

mysql-integration-test:
<<: *mysql-common
ports:
- 3307:3306

pma:
image: phpmyadmin/phpmyadmin
Expand Down
61 changes: 37 additions & 24 deletions lib/Default/MySqlDatabase.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,29 @@ abstract class MySqlDatabase {
protected static $selectedDbName;
protected $dbResult = null;
protected $dbRecord = null;

public function __construct($dbName) {
if (!self::$dbConn) {
// Set the mysqli driver to raise exceptions on errors
if (!mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT)) {
$this->error('Failed to enable mysqli error reporting');
}

self::$dbConn = new mysqli(self::$host, self::$user, self::$password,
$dbName, self::$port, self::$socket);
$host = self::$host;
$user = self::$user;
$password = self::$password;
$port = self::$port;

// The configuration can be overridden via PHPUnit configuration
if (defined("OVERRIDE_MYSQL_CONFIG")) {
$host = constant("OVERRIDE_MYSQL_HOST");
$user = constant("OVERRIDE_MYSQL_USER");
$password = constant("OVERRIDE_MYSQL_PASSWORD");
$port = (int)constant("OVERRIDE_MYSQL_PORT");
}

self::$dbConn = new mysqli($host, $user, $password,
$dbName, $port, self::$socket);
self::$selectedDbName = $dbName;

// Default server charset should be set correctly. Using the default
Expand Down Expand Up @@ -52,7 +65,7 @@ public function close() {
self::$dbConn = false;
}
}

public function query($query) {
$this->dbResult = self::$dbConn->query($query);
}
Expand All @@ -64,7 +77,7 @@ public function nextRecord() : bool {
if (!$this->dbResult) {
$this->error('No resource to get record from.');
}

if ($this->dbRecord = $this->dbResult->fetch_assoc()) {
return true;
}
Expand All @@ -88,7 +101,7 @@ public function hasField($name) {
public function getField($name) {
return $this->dbRecord[$name];
}

public function getBoolean($name) {
if ($this->dbRecord[$name] == 'TRUE') {
return true;
Expand All @@ -97,15 +110,15 @@ public function getBoolean($name) {
return false;
// throw new Exception('Field is not a boolean');
}

public function getInt($name) {
return (int)$this->dbRecord[$name];
}

public function getFloat($name) {
return (float)$this->dbRecord[$name];
}

// WARNING: In the past, Microtime was stored in the database incorrectly.
// For backwards compatibility, set $pad_msec=true to try to guess at the
// intended value. This is not safe if the Microtime length is wrong for an
Expand All @@ -123,43 +136,43 @@ public function getMicrotime($name, $pad_msec = false) {
}
return "$sec.$msec";
}

public function getObject($name, $compressed = false) {
$object = $this->getField($name);
if ($compressed === true) {
$object = gzuncompress($object);
}
return unserialize($object);
}

public function getRow() {
return $this->dbRecord;
}

public function lockTable($table) {
self::$dbConn->query('LOCK TABLES ' . $table . ' WRITE');
}

public function unlock() {
self::$dbConn->query('UNLOCK TABLES');
}

public function getNumRows() {
return $this->dbResult->num_rows;
}

public function getChangedRows() {
return self::$dbConn->affected_rows;
}

public function getInsertID() {
return self::$dbConn->insert_id;
}

protected function error($err) {
throw new Exception($err);
}

public function escape($escape, $autoQuotes = true, $quotes = true) {
if (is_bool($escape)) {
if ($autoQuotes) {
Expand Down Expand Up @@ -189,7 +202,7 @@ public function escape($escape, $autoQuotes = true, $quotes = true) {
}
}
}

public function escapeString($string, $quotes = true, $nullable = false) {
if ($nullable === true && ($string === null || $string === '')) {
return 'NULL';
Expand All @@ -211,11 +224,11 @@ public function escapeString($string, $quotes = true, $nullable = false) {
}
return self::$dbConn->real_escape_string($string);
}

public function escapeBinary($binary) {
return '0x' . bin2hex($binary);
}

public function escapeArray(array $array, $autoQuotes = true, $quotes = true, $implodeString = ',', $escapeIndividually = true) {
$string = '';
if ($escapeIndividually) {
Expand All @@ -232,7 +245,7 @@ public function escapeArray(array $array, $autoQuotes = true, $quotes = true, $i
}
return $string;
}

public function escapeNumber($num) {
// Numbers need not be quoted in MySQL queries, so if we know $num is
// numeric, we can simply return its value (no quoting or escaping).
Expand All @@ -242,13 +255,13 @@ public function escapeNumber($num) {
throw new Exception('Not a number! (' . $num . ')');
}
}

public function escapeMicrotime($microtime, $quotes = false) {
$sec_str = sprintf('%010d', $microtime);
$usec_str = sprintf('%06d', fmod($microtime, 1) * 1E6);
return $this->escapeString($sec_str . $usec_str, $quotes);
}

public function escapeBoolean($bool, $quotes = true) {
if ($bool === true) {
return $this->escapeString('TRUE', $quotes);
Expand All @@ -258,7 +271,7 @@ public function escapeBoolean($bool, $quotes = true) {
throw new Exception('Not a boolean: ' . $bool);
}
}

public function escapeObject($object, $compress = false, $quotes = true, $nullable = false) {
if ($compress === true) {
return $this->escapeBinary(gzcompress(serialize($object)));
Expand Down
Loading

0 comments on commit 0710abf

Please sign in to comment.