Commit e9b52cf5 authored by Patrick Barroca's avatar Patrick Barroca 🐧

first commit

parents
/vendor/
/logs/*
!/logs/README.md
This diff is collapsed.
# OAI-Browser
This project aim to provide UI to easily browse OAI-PMH servers
## Install
Clone this project
```bash
git clone git@git.afi-sa.net:afi/oai-browser.git
```
This project use Composer to manage its dependencies (see https://getcomposer.org/)
Go into directory and download dependencies
```bash
cd oai-browser
composer install
```
To run in development from within project directory
```bash
composer start
```
## Testing
```bash
composer test
```
## License
Copyright (c) 2019 Agence Française Informatique (AFI)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.html>.
{
"name": "afi/oai-browser",
"description": "OAI-PMH Browser",
"keywords": ["oai-pmh", "harvest"],
"homepage": "https://git.afi-sa.net/afi/oai-browser",
"license": "AGPL-3.0-only",
"authors": [
{
"name": "Patrick Barroca",
"email": "pbarroca@afi-sa.fr"
}
],
"require": {
"php": ">=5.6",
"guzzlehttp/guzzle": "^6.3",
"monolog/monolog": "^1.17",
"slim/php-view": "^2.0",
"slim/slim": "^3.1"
},
"require-dev": {
"phpunit/phpunit": ">=5.0"
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"config": {
"process-timeout": 0,
"sort-packages": true
},
"scripts": {
"start": "php -S localhost:8080 -t public",
"test": "phpunit"
}
}
This diff is collapsed.
Your Slim Framework application's log files will be written to this directory.
<phpunit bootstrap="vendor/autoload.php">
<testsuites>
<testsuite name="OaiBrowser">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
<IfModule mod_rewrite.c>
RewriteEngine On
# Some hosts may require you to use the `RewriteBase` directive.
# Determine the RewriteBase automatically and set it as environment variable.
# If you are using Apache aliases to do mass virtual hosting or installed the
# project in a subdirectory, the base path will be prepended to allow proper
# resolution of the index.php file and to redirect to the correct URI. It will
# work in environments without path prefix as well, providing a safe, one-size
# fits all solution. But as you do not need it in this case, you can comment
# the following 2 lines to eliminate the overhead.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
# If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the
# absolute physical path to the directory that contains this htaccess file.
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
</IfModule>
<?php
if (PHP_SAPI == 'cli-server') {
// To help the built-in PHP dev server, check if the request was actually for
// something which should probably be served as a static file
$url = parse_url($_SERVER['REQUEST_URI']);
$file = __DIR__ . $url['path'];
if (is_file($file)) {
return false;
}
}
require __DIR__ . '/../vendor/autoload.php';
session_start();
// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);
// Set up dependencies
$dependencies = require __DIR__ . '/../src/dependencies.php';
$dependencies($app);
// Register middleware
$middleware = require __DIR__ . '/../src/middleware.php';
$middleware($app);
// Register routes
$routes = require __DIR__ . '/../src/routes.php';
$routes($app);
// Run app
$app->run();
<?php
/**
* OAI-Browser, Easily browse OAI-PMH servers.
*
* Copyright (c) 2019 Agence Française Informatique (AFI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.html>.
*/
use Slim\App;
return function (App $app) {
$container = $app->getContainer();
// view renderer
$container['renderer'] = function ($c) {
$settings = $c->get('settings')['renderer'];
return new \Slim\Views\PhpRenderer($settings['template_path']);
};
$container['errorHandler'] = function($c) {
return function($request, $response, $exception) use ($c) {
return $c->get('renderer')->render($response, 'error.phtml',
['exception' => $exception]);
};
};
// monolog
$container['logger'] = function ($c) {
$settings = $c->get('settings')['logger'];
$logger = new \Monolog\Logger($settings['name']);
$logger->pushProcessor(new \Monolog\Processor\UidProcessor());
$logger->pushHandler(new \Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
return $logger;
};
};
<?php
/**
* OAI-Browser, Easily browse OAI-PMH servers.
*
* Copyright (c) 2019 Agence Française Informatique (AFI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.html>.
*/
use Slim\App;
return function (App $app) {
// e.g: $app->add(new \Slim\Csrf\Guard);
};
<?php
/**
* OAI-Browser, Easily browse OAI-PMH servers.
*
* Copyright (c) 2019 Agence Française Informatique (AFI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.html>.
*/
use Slim\App;
use Slim\Http\Request;
use Slim\Http\Response;
return function (App $app) {
$container = $app->getContainer();
$client = new \GuzzleHttp\Client();
$verb_to_xml = function($url, $verb, $other_params=[]) use($client) {
$url .= '?verb=' . $verb;
if ($other_params)
$url .= '&' . http_build_query($other_params);
$response = $client->request('GET', $url);
$xml = new SimpleXMLElement($response->getBody());
if ($error = $xml->error)
throw new RuntimeException($error['code']);
return $xml->$verb;
};
$app->get(
'/list/{set}/{format}',
function (Request $request, Response $response, array $args) use ($container, $verb_to_xml) {
if (!$url = $_SESSION['url'])
throw new RuntimeException('Lost endpoint url');
if (!$format = $args['format'])
throw new RuntimeException('Cannot list records without format');
if (!$set = $args['set'])
throw new RuntimeException('Cannot list set without it');
$other_params = ['metadataPrefix' => $format,
'set' => $set];
return $container->get('renderer')
->render($response, 'list.phtml',
['records' => $verb_to_xml($url, 'ListRecords', $other_params),
'url' => $url . '?verb=ListRecords' . http_build_query($other_params),
'format' => $format]);
}
);
$app->get(
'/list/{format}',
function (Request $request, Response $response, array $args) use ($container, $verb_to_xml) {
if (!$url = $_SESSION['url'])
throw new RuntimeException('Lost endpoint url');
if (!$format = $args['format'])
throw new RuntimeException('Cannot list records without format');
return $container->get('renderer')
->render($response, 'list.phtml',
['records' => $verb_to_xml($url, 'ListRecords',
['metadataPrefix' => $format]),
'url' => $url . '?verb=ListRecords&metadataPrefix=' . $format,
'format' => $format]);
}
);
$app->get(
'/resume/{token}',
function (Request $request, Response $response, array $args) use ($container, $verb_to_xml) {
if (!$url = $_SESSION['url'])
throw new RuntimeException('Lost endpoint url');
if (!$token = $args['token'])
throw new RuntimeException('Cannot resume without token');
return $container->get('renderer')
->render($response, 'list.phtml',
['records' => $verb_to_xml($url, 'ListRecords',
['resumptionToken' => $token]),
'url' => $url . '?verb=ListRecords&resumptionToken=' . $token]);
}
);
$app->get(
'/describe',
function(Request $request, Response $response, array $args) use ($container, $verb_to_xml) {
if (!$url = $_SESSION['url'])
throw new RuntimeException('Lost endpoint url');
$identity = $verb_to_xml($url, 'Identify');
$sets = $verb_to_xml($url, 'ListSets');
$formats = $verb_to_xml($url, 'ListMetadataFormats');
return $container->get('renderer')
->render($response, 'description.phtml',
['identity' => $identity,
'sets' => $sets,
'formats' => $formats,
'url' => $url]);
}
);
$app->get(
'/',
function (Request $request, Response $response, array $args) use($container) {
if ($url = $request->getParam('url')) {
$_SESSION['url'] = $url;
return $response->withRedirect('/describe', 301);
}
return $container->get('renderer')
->render($response, 'index.phtml');
}
);
};
<?php
/**
* OAI-Browser, Easily browse OAI-PMH servers.
*
* Copyright (c) 2019 Agence Française Informatique (AFI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.html>.
*/
return [
'settings' => [
'displayErrorDetails' => true, // set to false in production
'addContentLengthHeader' => false, // Allow the web server to send the content-length header
// Renderer settings
'renderer' => [
'template_path' => __DIR__ . '/../templates/',
],
// Monolog settings
'logger' => [
'name' => 'slim-app',
'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
'level' => \Monolog\Logger::DEBUG,
],
],
];
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>OAI-PMH Browser</title>
</head>
<body>
<div class="row col-12 m-4">
<h1>Endpoint : <?php echo htmlspecialchars($url); ?></h1>
</div>
<div class="row col-12 m-4">
<p>
<a class="btn btn-primary" href="/" role="button">Back home</a>
</p>
</div>
<div class="row col-12 m-4">
<?php if ($identity->getName()) { ?>
<div class="col-md-4 col-sm-12">
<div class="card">
<div class="card-header">Identity</div>
<div class="card-body">
<dl>
<dt>Name</dt>
<dd><?php echo htmlspecialchars($identity->repositoryName); ?></dd>
<dt>Base URL</dt>
<dd><?php echo htmlspecialchars($identity->baseURL); ?></dd>
<dt>Protocol version</dt>
<dd><?php echo htmlspecialchars($identity->protocolVersion); ?></dd>
<dt>Admin email</dt>
<dd><?php echo htmlspecialchars($identity->adminEmail); ?></dd>
<dt>Date granularity</dt>
<dd><?php echo htmlspecialchars($identity->granularity); ?></dd>
<dt>Earliest datestamp</dt>
<dd><?php echo htmlspecialchars($identity->earliestDatestamp); ?></dd>
<dt>Deleted record strategy</dt>
<dd><?php echo htmlspecialchars($identity->deletedRecord); ?></dd>
</dl>
</div>
</div>
</div>
<?php } ?>
<?php if ($sets->getName()) { ?>
<div class="col-md-4 col-sm-12">
<div class="card">
<div class="card-header">Sets</div>
<div class="card-body">
<dl>
<?php foreach($sets->set as $set) { ?>
<dt><?php echo htmlspecialchars($set->setSpec);?></dt>
<dd>
<?php echo htmlspecialchars($set->setName); ?><br>
<?php foreach($formats->metadataFormat as $format) { ?>
<a class="btn btn-primary"
href="/list/<?php echo htmlspecialchars($set->setSpec);?>/<?php echo htmlspecialchars($format->metadataPrefix);?>"
role="button">
<?php echo htmlspecialchars($format->metadataPrefix);?>
</a>
<?php } ?>
</dd>
<?php } ?>
</dl>
</div>
</div>
</div>
<?php } ?>
<?php if ($formats->getName()) { ?>
<div class="col-md-4 col-sm-12">
<div class="card">
<div class="card-header">Metadata formats</div>
<div class="card-body">
<dl>
<?php foreach($formats->metadataFormat as $format) { ?>
<dt><?php echo htmlspecialchars($format->metadataPrefix);?></dt>
<dd>
<?php echo htmlspecialchars($format->schema); ?><br>
<a class="btn btn-primary"
href="/list/<?php echo htmlspecialchars($format->metadataPrefix);?>"
role="button">List</a>
</dd>
<?php } ?>
</dl>
</div>
</div>
</div>
<?php } ?>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>ERROR - OAI-PMH Browser</title>
</head>
<body>
<div class="row col-12 m-4">
<h1>Error <a class="btn btn-primary" href="/" role="button">Back home</a></h1>
</div>
<?php if ($exception) { ?>
<div class="row col-12 m-4">
<?php echo htmlspecialchars($exception->getMessage());?>
</div>
<?php } ?>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>OAI-PMH Browser</title>
</head>
<body>
<div class="row col-12 m-4">
<h1>Welcome</h1>
</div>
<div class="row col-12 m-4">
<form>
<div class="form-group">
<p>Start by providing endpoint</p>
<div class="input-group mb-3">
<input type="url" class="form-control" placeholder="endpoint url" name="url">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">GO !</button>
</div>
</div>
</div>
</form>
</div>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
<!doctype html>
<html lang="fr">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>OAI-PMH Browser</title>
</head>
<body>
<div class="row col-12 m-4">
<h1>List <a class="btn btn-primary" href="/describe" role="button">Back to description</a></h1>
</div>
<div class="row col-12 m-4">
<h2><?php echo htmlspecialchars($url);?></h2>
<div class="card">
<div class="card-header">Status</div>
<div class="card-body">
<dl>
<dt>Records on page</dt>
<dd><?php echo $records->record->count();?></dd>
<?php if ('' != ($token = $records->resumptionToken)) { ?>
<dt>Records total</dt>
<dd>
<?php echo $records->resumptionToken['completeListSize'];?><br>
<a class="btn btn-primary"
href="/resume/<?php echo htmlspecialchars($token);?>"
role="button">Next</a>
</dd>
<dt>Last record in this page is </dt>
<dd><?php echo $records->resumptionToken['cursor'];?></dd>
<?php } ?>
</dl>
</div>
</div>
<div class="row col-12 m-4">
<?php foreach($records->record as $record) {
$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadXML($record->metadata->asXML());
?>
<div class="card">
<div class="card-header">
<?php echo htmlspecialchars($record->header->identifier
. ' (' . $record->header->datestamp. ')');?>
</div>
<div class="card-body">
<pre style="overflow:scroll;"><?php echo htmlspecialchars($dom->saveXML());?></pre>
</div>
</div>
<?php } ?>
</div>
<pre>
<?php echo htmlspecialchars($records->asXml());?>
</pre>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
<?php
namespace Tests\Functional;
use Slim\App;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Environment;
use PHPUnit\Framework\TestCase;
/**
* This is an example class that shows how you could set up a method that
* runs the application. Note that it doesn't cover all use-cases and is
* tuned to the specifics of this skeleton app, so if your needs are
* different, you'll need to change it.
*/
class BaseTestCase extends TestCase
{
/**
* Use middleware when running application?
*
* @var bool
*/
protected $withMiddleware = true;
/**
* Process the application given a request method and URI
*
* @param string $requestMethod the request method (e.g. GET, POST, etc.)
* @param string $requestUri the request URI
* @param array|object|null $requestData the request data
* @return \Slim\Http\Response
*/
public function runApp($requestMethod, $requestUri, $requestData = null)
{
// Create a mock environment for testing with
$environment = Environment::mock(
[
'REQUEST_METHOD' => $requestMethod,
'REQUEST_URI' => $requestUri
]
);
// Set up a request object based on the environment
$request = Request::createFromEnvironment($environment);
// Add request data, if it exists
if (isset($requestData)) {
$request = $request->withParsedBody($requestData);
}
// Set up a response object
$response = new Response();
// Use the application settings
$settings = require __DIR__ . '/../../src/settings.php';
// Instantiate the application
$app = new App($settings);
// Set up dependencies
$dependencies = require __DIR__ . '/../../src/dependencies.php';
$dependencies($app);
// Register middleware
if ($this->withMiddleware) {
$middleware = require __DIR__ . '/../../src/middleware.php';
$middleware($app);
}
// Register routes
$routes = require __DIR__ . '/../../src/routes.php';
$routes($app);
// Process the application
$response = $app->process($request, $response);
// Return the response
return $response;
}
}
<?php
namespace Tests\Functional;
class HomepageTest extends BaseTestCase
{
/** @test */