Browse Source

Merge branch 'feature/refactor_ot_api_calls' into develop

Olivier Massot 4 năm trước cách đây
mục cha
commit
73bc3047e4

+ 0 - 1
composer.json.txt

@@ -1 +0,0 @@
-@See docker/conf/composer.json

+ 93 - 21
doc/problems_resolutions/cross_domain_auth.md

@@ -16,54 +16,126 @@ Les cookies générés par l'authentif auprès de l'api sont donc invisibles pou
 
 Pour tester les solutions, il va falloir simuler un domaine différent.
 
+Dans le cas standard, si je me rend à l'adresse <local.admin.opentalent.fr/#/login>, et que je me connecte 
+en tant que opentalent74, lorsque je me rend à l'adresse <local.sub.opentalent.fr/ohcluses>, 
+j'apparais comme connecté.
+
+
 Je créé l'entrée suivante dans mon /etc/hosts:
 
-    127.0.0.1 local.sub.mydomain.fr
+    127.0.0.1 local.sub.customdomain.fr
+
+Je créé un certificat pour l'adresse local.sub.customdomain.fr
 
 Je me connecte au docker nginx-proxy, et je remplace les domaines dans la conf de nginx:
 
-    sed -i 's/local\.sub\.opentalent\.fr/local.sub.mydomain.fr/g' /etc/nginx/default.conf
+    sed -i 's/local\.sub\.opentalent\.fr/local.sub.customdomain.fr/g' /etc/nginx/conf.d/default.conf
     nginx -s reload
 
 Je me rend à l'adresse local.admin.opentalent.fr/#/login
 
 Je m'authentifie en tant que opentalent74
 
-Je me rend à l'adresse http://local.sub.mydomain.fr/ohcluses
+Je me rend à l'adresse http://local.sub.customdomain.fr/ohcluses
 
 Je ne suis pas connecté.
 
-Je reviens à l'url standard:
 
-Je me connecte au docker nginx-proxy, et je remplace les domaines dans la conf de nginx:
+## Solutions envisagées
 
-    sed -i 's/local\.sub\.mydomain\.fr/local.sub.opentalent.fr/g' /etc/nginx/default.conf
-    nginx -s reload
+### 1- Envoyer les cookies au site depuis l'api via un controller dédié
 
+Lors d'une connexion réussie, l'API enverra une requête POST aux sites ayant des domaines custom et pour lesquelles
+le user a un Access
 
+Un controller dédié côté Typo3 (ex: setCookies.php) génèrera ensuite les cookies avec les noms de domaines correspondant.
 
-## Solutions envisagées
+Pour tester cette solution, j'ajoute une requête curl dans le AuthenticationSuccessListener:
 
-### 
+    http://docker.sub.customdomain.fr/typo3conf/ext/ot_connect/setCookies.php?BEARER=' . $data['token']
 
+Côté setCookie.php, le contenu est simplement:
 
-Lors d'une connexion réussie, l'API enverra une requête POST aux sites ayant des domaines custom et pour lesquelles
-le user a un Access
+    setcookie('BEARER', $_REQUEST['BEARER'], 0, "/", "customdomain.fr");
 
-Un controller dédié côté Typo3 (ex: setCookies.php) génèrera ensuite les cookies avec les noms de domaines correspondant.
+Je teste, je m'assure que:
+
+* le fichier setCookie est bien appelé: oui 
+* la variable `$_REQUEST['BEARER']` est bien définie: oui
+
+Je teste dans mon navigateur:
+
+* je vide mes cookies
+* je me rend à https://local.admin.opentalent.fr/#/login
+* je me connecte en tant que opentalent74
+* je me rend à l'adresse https://local.sub.customdomain.fr/ohcluses
+* je ne suis pas authentifié, et aucun cookie n'apparait.
+
+Raté.
+
+### 2- SetCookie + <img>
+
+Plus de détails sur la solution ici: https://subinsb.com/set-same-cookie-on-different-domains/
+
+Le setcookie.php est de la forme:
+
+    setcookie('BEARER', '123456', 0, "/", "customdomain.fr");
+
+J'ajoute la ligne suivante au front du logiciel:
+
+    <img src="http://local.sub.customdomain.fr/typo3conf/ext/ot_connect/setCookies.php" style="display:none;" />
+
+Je teste de la même façon que pour la solution 1
+
+Le cookie est bien présent: Yes!
+
+
+### 2- SetCookie + <img>
+
+J'ajoute à docker/apps/opentalent-admin-2.0/src/app/config/routing/main.js, ligne 79 :
+
+    setCookie:['getOrganization', 'Restangular', function(getOrganization, Restangular){
+        Restangular.oneUrl('no-x-access-id', 'https://local.sub.customdomain.fr')
+            .withHttpConfig({withCredentials: false})
+            .get()
+            .then(resp => {
+            })
+    }],
+
+Afin de tester plus facilement depuis la page de login du logiciel, je modifie la ligne 14 du fichier 
+docker/apps/opentalent-admin-2.0/src/app/ng-admin-jwt-auth/loginController.js en:
+
+    this.$state.go('switch', { organization_id: response.data.profile.organizationConnected}, {'reload':true, 'inherit':false});
+
+Je relance le `gulp serve`
+
+Le setcookie.php est de la forme:
+
+    setcookie('BEARER', 'azerty', 0, "/", "customdomain.fr");
+
+Je teste de la même façon que pour la solution 1
+
+(...)
+
+Après de nombreuses tentatives et blocages (blocages CORS, variable _POST vide...)
+On décide de laisser tomber cette méthode pour le moment.
+
+
+## Solution retenue et mise en oeuvre
+
+On récupère le champs otherWebsite de la structure
 
+Si ce champs ne matche pas la regex `https?:\/\/.*\.opentalent\.fr`
 
-Etapes:
+Alors, on insère la ligne:
 
-1. Le nom de domaine custom doit être stocké dans le champs Parameters.website:
-* vérifier le contenu actuel
-* reprise des custom_domains dans typo
-* automatiser le tout
-2. Créer un script setCookie dans OtConnect qui recevrait les requêtes post provenant de l'API
-3. Ajouter un hook lors du apiSuccess de l'api pour envoyer la requête post
+    <img src="https://<domain>/typo3conf/ext/ot_connect/setCookies.php?bearer=<bearer>" alt="" style="display:none;" />
 
-La requête envoyée à setCookie doit contenir simplement le BEARER du user nouvellement authentifié.
+où:
+* <domain> est le champs otherWebsite de la table Parameters de la structure à laquelle le user est connecté
+* <bearer> est le token bearer du user connecté
 
-Le fichier setCookie.fr doit générer un cookie correspondant à ce bearer et le renvoyer
+Le setCookie appelé vérifie que le referer est bien en opentalent.fr. Si oui, et si
+la requête a un paramètre BEARER, alors il créé le cookie correspondant dans le bon domaine.
 
 

+ 18 - 26
ot_connect/Classes/Service/OtAuthenticationService.php

@@ -2,11 +2,12 @@
 namespace Opentalent\OtConnect\Service;
 
 use DateTime;
-use GuzzleHttp\Client;
 use GuzzleHttp\Cookie\CookieJar;
 use GuzzleHttp\Cookie\SetCookie;
 use GuzzleHttp\Exception\GuzzleException;
 use GuzzleHttp\Exception\RequestException;
+use Opentalent\OtCore\Exception\ApiRequestException;
+use Opentalent\OtCore\Service\OpentalentApiService;
 use TYPO3\CMS\Core\Crypto\Random;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
@@ -19,16 +20,13 @@ use \TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
  */
 class OtAuthenticationService extends AbstractAuthenticationService
 {
-
-    CONST DOMAIN = 'https://api.opentalent.fr';
-    CONST API_URI = self::DOMAIN . '/api/';
-    CONST LOGIN_URI = self::API_URI . 'login_check';
-    CONST GET_USER_DATA_URI = self::API_URI . 'user/datafortypo3';
-    CONST ISAUTH_URI = self::API_URI . 'user/isauthenticated';
-    CONST LOGOUT_URI = self::API_URI . 'logout';
+    CONST LOGIN_URI = 'api/login_check';
+    CONST GET_USER_DATA_URI = 'api/user/datafortypo3';
+    CONST ISAUTH_URI = 'api/user/isauthenticated';
+    CONST LOGOUT_URI = 'api/logout';
     CONST GROUP_FE_ALL_UID = 18076;
 
-    // Cookies'domain needs to be the same that the api's cookies, or guzzle will ignore them.
+    // Cookies' domain needs to be the same that the api's cookies, or guzzle will ignore them.
     CONST COOKIE_DOMAIN = 'opentalent.fr';
 
     CONST PRODUCT_MAPPING = [
@@ -70,12 +68,9 @@ class OtAuthenticationService extends AbstractAuthenticationService
     const STATUS_AUTHENTICATION_SUCCESS = 200;
 
     /**
-     * Guzzle Client
-     *
-     * @see http://docs.guzzlephp.org/en/stable/
-     * @var Client
+     * @var object
      */
-    private Client $client;
+    private object $apiService;
 
     /**
      * Guzzle Cookie Jar
@@ -99,7 +94,7 @@ class OtAuthenticationService extends AbstractAuthenticationService
      */
     public function __construct() {
         $this->jar = new CookieJar;
-        $this->client = new Client(['base_uri' => self::DOMAIN, 'cookies' => $this->jar]);
+        $this->apiService = GeneralUtility::makeInstance(OpentalentApiService::class, null, null, $this->jar);
         $this->connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
     }
 
@@ -177,7 +172,7 @@ class OtAuthenticationService extends AbstractAuthenticationService
     {
         $this->fillCookieJar();
         try {
-            $response = $this->client->request('GET', self::ISAUTH_URI, ['cookies' => $this->jar]);
+            $response = $this->apiService->get(self::ISAUTH_URI, [], ['cookies' => $this->jar]);
 
             if ($response->getStatusCode() != 200) {
                 return null;
@@ -185,10 +180,9 @@ class OtAuthenticationService extends AbstractAuthenticationService
 
             return json_decode((string)$response->getBody());
 
-        } catch (RequestException $e) {
+        } catch (ApiRequestException $e) {
             return null;
         }
-
     }
 
     /**
@@ -221,7 +215,7 @@ class OtAuthenticationService extends AbstractAuthenticationService
     {
 
         try {
-            $response = $this->client->request(
+            $response = $this->apiService->request(
                 'POST',
                 self::LOGIN_URI,
                 ['form_params' => ['_username' => $username, '_password' => $password]]
@@ -237,7 +231,7 @@ class OtAuthenticationService extends AbstractAuthenticationService
             $this->setCookiesFromApiResponse($response);
             return true;
 
-        } catch (RequestException $e) {
+        } catch (ApiRequestException $e) {
             return false;
         }
     }
@@ -353,14 +347,12 @@ class OtAuthenticationService extends AbstractAuthenticationService
      *
      * @return array
      */
-    protected function getUserData(): array
+    protected function getUserData(): ?array
     {
         $this->fillCookieJar();
         try {
-            $response = $this->client->request('GET', self::GET_USER_DATA_URI, ['cookies' => $this->jar]);
-        } catch (RequestException $e) {
-            return [];
-        } catch (GuzzleException $e) {
+            $response = $this->apiService->request('GET', self::GET_USER_DATA_URI, [], ['cookies' => $this->jar]);
+        } catch (ApiRequestException $e) {
             return [];
         }
         return json_decode($response->getBody(), true);
@@ -401,7 +393,7 @@ class OtAuthenticationService extends AbstractAuthenticationService
     public function logout(): bool
     {
         try {
-            $response = $this->client->request(
+            $response = $this->apiService->request(
                 'GET',
                 self::LOGOUT_URI
             );

+ 16 - 0
ot_connect/setCookies.php

@@ -0,0 +1,16 @@
+<?php
+
+/**
+ * This script will set cookies for the websites with a custom domain (not in '.opentalent.fr')
+ *
+ * NB: It is voluntarily kept separated from typo3, in order to stay as light as possible,
+ *      because it will be called from the Opentalent front app.
+ */
+
+if (
+    preg_match("/https?:\/\/(.*\.)?opentalent.fr(\/.*)?/", $_SERVER['HTTP_REFERER'])
+    && isset($_REQUEST['BEARER'])
+)
+{
+    setcookie('BEARER', $_REQUEST['BEARER'], 0, "/");
+}

+ 36 - 138
ot_core/Classes/Domain/Repository/BaseApiRepository.php

@@ -5,12 +5,14 @@ namespace Opentalent\OtCore\Domain\Repository;
 use GuzzleHttp\Client;
 use GuzzleHttp\Exception\GuzzleException;
 use Opentalent\OtCore\Exception\ApiRequestException;
+use Opentalent\OtCore\Service\OpentalentApiService;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use Symfony\Component\Yaml\Exception\ParseException;
 use Symfony\Component\Yaml\Yaml;
 use TYPO3\CMS\Core\Core\ApplicationContext;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
 
 /**
@@ -21,117 +23,25 @@ abstract class BaseApiRepository implements LoggerAwareInterface
 {
     use LoggerAwareTrait;
 
-    const DEFAULT_BASE_URI = 'https://api.opentalent.fr/api/';
     const URI_TRAILING_PART = '';
     const HYDRA_TYPE = '';
-    const HTTP_METHOD = 'GET';
     const DEFAULT_ITEMS_PER_PAGE = 8;
 
-    protected string $base_uri = self::DEFAULT_BASE_URI;
-    protected array $variants_uris = [];
-    protected Client $client;
-    protected ApplicationContext $context;
+    protected $apiService;
 
     /**
      * BaseApiRepository constructor.
-     *
-     * @param ObjectManagerInterface $objectManager
-     * @param Client|null $client  [For tests only]
-     * @param ApplicationContext|null $context  [For tests only]
-     */
-    public function __construct(
-        ?Client $client = null,
-        ?ApplicationContext $context = null
-    ) {
-        if ($context === null) {
-            $this->context = \TYPO3\CMS\Core\Core\Environment::getContext();
-        } else {
-            $this->context = $context;
-        }
-
-        $this->loadConf();
-
-        if ($client === null) {
-            $this->client = new Client(['base_uri' => $this->getApiUri()]);
-        } else {
-            $this->client = $client;
-        }
-    }
-
-    private function loadConf() {
-        $conf_path = $_ENV['TYPO3_PATH_ROOT'] . '/typo3conf/ext/ot_core/Configuration/ot_config.yaml';
-        $conf = Yaml::parseFile($conf_path);
-
-        // api_variant_uri: Should we set an alternative uri for the API? (dev and testing only)
-        if ($this->context->isDevelopment() || $this->context->isTesting()) {
-            $this->variants_uris = $conf['api_variant_uri'];
-        }
-    }
-
-    /**
-     * Return the API URI for the current repository
-     *
-     * @param string $trailing_part
-     * @return string
      */
-    protected function getApiUri(string $trailing_part = null): string
-    {
-        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['VIRTUAL_HOST'];
-
-        if (isset($this->variants_uris[$host])) {
-            $uri = $this->variants_uris[$host];
-        } else {
-            $uri = self::DEFAULT_BASE_URI;
-        }
-
-        $trailing_part = $trailing_part ?? $this::URI_TRAILING_PART;
-        $uri = rtrim($uri, '/') . '/' . ltrim($trailing_part, '/');
-        return $uri;
+    public function __construct($apiService = null) {
+        $this->apiService = $apiService ?? GeneralUtility::makeInstance(OpentalentApiService::class);
     }
 
     /**
      * [FOR TESTS ONLY]
      * @param Client $client
      */
-    protected function injectClient(Client $client) {
-        $this->client = $client;
-    }
-
-    /**
-     * Send a request to the API and
-     * returns the records as an array (members)
-     *
-     * @param array $params
-     * @param string|null $forceUri
-     * @return ApiPagedCollection
-     * @throws ApiRequestException
-     */
-    protected function getApiRecords(array $params = [], ?string $forceUri = null): ApiPagedCollection
-    {
-        $uri = $forceUri ?? $this->getApiUri();
-
-        $body = $this->getJsonDecoded($uri, $params);
-
-        $page = (int)($params['page'] ?? 1);
-
-        // build up the members
-        $members = [];
-
-        if (is_array($body['hydra:member'])) {
-            foreach ($body['hydra:member'] as $record) {
-                $instance = $this->memberToObject($record);
-                if ($instance != null) {
-                    $members[] = $instance;
-                }
-            }
-        }
-
-        return new ApiPagedCollection(
-            (int)($body['hydra:totalItems'] ?? 0),
-            (int)($body['hydra:itemsPerPage'] ?? 0),
-            $page,
-            $members
-        );
+    protected function injectService(OpentalentApiService $apiService) {
+        $this->apiService = $apiService;
     }
 
     /**
@@ -161,57 +71,45 @@ abstract class BaseApiRepository implements LoggerAwareInterface
 
     /**
      * Send a request to the API and
-     * returns the Json response as an array
-     *
-     * @param string $uri
-     * @param array $params
-     * @return array
-     * @throws ApiRequestException
-     */
-    protected function getJsonDecoded(string $uri, $params = []): array
-    {
-        return json_decode($this->getBody($uri, $params),true);
-    }
-
-    /**
-     * Send a request to the API and returns
-     * the response's body as a string
-     *
-     * @param string $uri
-     * @param array $params
-     * @return string
-     * @throws ApiRequestException
-     */
-    protected function getBody(string $uri, $params = [])
-    {
-        return (string)$this->getResponse($uri, $params)->getBody();
-    }
-
-    /**
-     * Send a request to the API and returns
-     * the result as a Response object
+     * returns the records as an array (members)
      *
-     * @param string $uri
      * @param array $params
-     * @return ResponseInterface
+     * @param string|null $forceUri
+     * @return ApiPagedCollection
      * @throws ApiRequestException
      */
-    protected function getResponse(string $uri, $params = []): ResponseInterface
+    protected function getApiRecords(array $params = [], ?string $forceUri = null): ApiPagedCollection
     {
-        $uri = $uri . '?_format=json';
+        $params = array("_format" => "json") + $params;   // _format should be the first param to match unit-tests fixtures
         if(!isset($params['itemsPerPage'])) {
             $params['itemsPerPage'] = (string)self::DEFAULT_ITEMS_PER_PAGE;
         }
-        if (!empty($params)) {
-            $uri = $uri . '&' . http_build_query($params);
+
+        $uri = ltrim($this::URI_TRAILING_PART, '/');
+        if ($forceUri !== null) {
+            $uri = trim($forceUri, '/') . '/' . $uri;
         }
-        try {
-            if ($this->context->isDevelopment()) {
-                $this->logger->info('API Call: ' . $uri);
+        $body = $this->apiService->getJsonDecoded($uri, $params);
+
+        $page = (int)($params['page'] ?? 1);
+
+        // build up the members
+        $members = [];
+
+        if (is_array($body['hydra:member'])) {
+            foreach ($body['hydra:member'] as $record) {
+                $instance = $this->memberToObject($record);
+                if ($instance != null) {
+                    $members[] = $instance;
+                }
             }
-            return $this->client->request(static::HTTP_METHOD, $uri);
-        } catch (GuzzleException $e) {
-            throw ApiRequestException::from_exception($e);
         }
+
+        return new ApiPagedCollection(
+            (int)($body['hydra:totalItems'] ?? 0),
+            (int)($body['hydra:itemsPerPage'] ?? 0),
+            $page,
+            $members
+        );
     }
 }

+ 1 - 1
ot_core/Classes/Domain/Repository/DonorRepository.php

@@ -7,7 +7,7 @@ use Opentalent\OtCore\Exception\ApiRequestException;
 
 class DonorRepository extends BaseApiRepository
 {
-    const URI_TRAILING_PART = 'public/donors';
+    const URI_TRAILING_PART = '/api/public/donors';
     const HYDRA_TYPE = 'PortailDonor';
 
     /**

+ 1 - 1
ot_core/Classes/Domain/Repository/EventRepository.php

@@ -8,7 +8,7 @@ use Opentalent\OtCore\Exception\ApiRequestException;
 
 class EventRepository extends BaseApiRepository
 {
-    const URI_TRAILING_PART = 'public/events';
+    const URI_TRAILING_PART = '/api/public/events';
     const HYDRA_TYPE = 'PortailEvent';
 
     /**

+ 9 - 0
ot_core/Classes/Domain/Repository/MemberCaRepository.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Opentalent\OtCore\Domain\Repository;
+
+class MemberCaRepository extends MemberRepository
+{
+    const URI_TRAILING_PART = '/api/public/members_ca';
+    const HYDRA_TYPE = 'PortailMemberByRole';
+}

+ 4 - 18
ot_core/Classes/Domain/Repository/MemberRepository.php

@@ -9,15 +9,8 @@ use Opentalent\OtCore\Exception\ApiRequestException;
 
 class MemberRepository extends BaseApiRepository
 {
-    const URI_TRAILING_PART = 'public/members';
-    const URI_TRAILING_PART_CA = 'public/members_ca';
+    const URI_TRAILING_PART = '/api/public/members';
     const HYDRA_TYPE = 'PortailMemberBySpeciality';
-    const HYDRA_TYPE_CA = 'PortailMemberByRole';
-
-    protected function getApiUriCa(): string
-    {
-        return $this->getApiUri(self::URI_TRAILING_PART_CA);
-    }
 
     /**
      * Get the members of the organization
@@ -28,17 +21,11 @@ class MemberRepository extends BaseApiRepository
      * @return ApiPagedCollection              Members
      * @throws ApiRequestException
      */
-    public function findByOrganizationId(int $organizationId,
-                                         $only_ca = false) {
+    public function findByOrganizationId(int $organizationId) {
         $params = [];
         $params['filter[where][organizationId]'] = $organizationId;
         $params['itemsPerPage'] = '200';
-
-        if ($only_ca) {
-            return $this->getApiRecords($params, $this->getApiUriCa());
-        } else {
-            return $this->getApiRecords($params);
-        }
+        return $this->getApiRecords($params);
     }
 
     /**
@@ -49,8 +36,7 @@ class MemberRepository extends BaseApiRepository
      * @throws \Exception
      */
     protected function memberToObject(array $record) {
-        if ($record['@type'] != $this::HYDRA_TYPE &&
-            $record['@type'] != $this::HYDRA_TYPE_CA) {
+        if ($record['@type'] != $this::HYDRA_TYPE) {
             return null;
         }
         $member = new Member();

+ 1 - 1
ot_core/Classes/Domain/Repository/OrganizationRepository.php

@@ -8,7 +8,7 @@ use Opentalent\OtCore\Exception\ApiRequestException;
 
 class OrganizationRepository extends BaseApiRepository
 {
-    const URI_TRAILING_PART = 'public/organizations';
+    const URI_TRAILING_PART = '/api/public/organizations';
     const HYDRA_TYPE = 'PortailOrganization';
 
     /**

+ 180 - 0
ot_core/Classes/Service/OpentalentApiService.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace Opentalent\OtCore\Service;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use Opentalent\OtCore\Exception\ApiRequestException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
+
+class OpentalentApiService implements LoggerAwareInterface
+{
+    use LoggerAwareTrait;
+
+    const DEFAULT_BASE_URI = 'https://api.opentalent.fr';
+    protected array $variants_uris = [
+          "preprod.opentalent.fr" => "https://api.preprod.opentalent.fr",
+          "local.sub.opentalent.fr" => "http://docker.nginx.opentalent.fr",
+          "typo3" => "http://docker.nginx.opentalent.fr"
+    ];
+
+    protected object $client;
+    protected object $context;
+
+    /**
+     * @param ObjectManagerInterface $objectManager
+     * @param object|null $client  [For tests only]
+     * @param object|null $context  [For tests only]
+     */
+    public function __construct(
+        ?object $client = null,
+        ?object $context = null,
+        ?object $cookieJar = null
+    ) {
+        // Get the current context (prod, dev...)
+        if ($context === null) {
+            $this->context = \TYPO3\CMS\Core\Core\Environment::getContext();
+        } else {
+            $this->context = $context;
+        }
+
+        if ($client === null) {
+            $args = ['base_uri' => $this->getApiUri()];
+            if ($cookieJar != null) {
+                $args['cookies'] = $cookieJar;
+            }
+            $this->client = new Client($args);
+        } else {
+            $this->client = $client;
+        }
+    }
+
+    public function getClient() {
+        return $this->client;
+    }
+
+    /**
+     * @param Client $client
+     */
+    public function injectClient(Client $client) {
+        $this->client = $client;
+    }
+
+    /**
+     * Return the API URI for the current repository
+     *
+     * @param string $trailing_part
+     * @return string
+     */
+    protected function getApiUri(string $trailing_part = ""): string
+    {
+        $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['VIRTUAL_HOST'];
+
+        if (isset($this->variants_uris[$host])) {
+            $uri = $this->variants_uris[$host];
+        } else {
+            $uri = self::DEFAULT_BASE_URI;
+        }
+
+        return rtrim($uri, '/') . '/' . ltrim($trailing_part, '/');
+    }
+
+    /**
+     * Send a request to the API and
+     * returns the Json response as an array
+     *
+     * @param string $uri
+     * @param array $params
+     * @return array
+     * @throws ApiRequestException
+     * @throws \JsonException
+     */
+    public function getJsonDecoded(
+        string $uri,
+        array $params = []
+    ): array
+    {
+        $body = $this->getBody($uri, $params);
+        $data = json_decode($body,true);
+        if ($data !== null) {
+            return json_decode($body,true);
+        } else {
+            throw new \JsonException('Response can not be decoded as json: ' . substr($body, 0, 300) . '...');
+        }
+    }
+
+    /**
+     * Send a request to the API and returns
+     * the response's body as a string
+     *
+     * @param string $uri
+     * @param array $params
+     * @return string
+     * @throws ApiRequestException
+     */
+    public function getBody(
+        string $uri,
+        array $params = []
+    ): string
+    {
+        return (string)$this->get($uri, $params)->getBody();
+    }
+
+    /**
+     * Send a GET request to the API and returns
+     * the result as a Response object
+     *
+     * @param string $uri
+     * @param array $params
+     * @return ResponseInterface
+     * @throws ApiRequestException
+     */
+    public function get(
+        string $uri,
+        array $params = [],
+        array $config = []
+    ): ResponseInterface
+    {
+        return $this->request('GET', $uri, $params, $config);
+    }
+
+    /**
+     * Send a request to the API and returns
+     * the result as a Response object
+     *
+     * @param string $httpMethod
+     * @param string $uri
+     * @param array $params
+     * @param array $config
+     * @return ResponseInterface
+     * @throws ApiRequestException
+     */
+    public function request(
+        string $httpMethod,
+        string $uri,
+        array $params = [],
+        array $config = []
+    ): ?ResponseInterface
+    {
+        $parsedUrl = parse_url($uri);
+        $params += ($parsedUrl['query'] ?? []);
+
+        $path = http_build_query($params);
+        $uri = rtrim($uri, '/');
+        if ($path) {
+            $uri .= '?' . $path;
+        }
+
+        try {
+            if ($this->context->isDevelopment()) {
+                $this->logger->info('API Call: ' . $uri);
+            }
+            return $this->client->request($httpMethod, $uri, $config);
+        } catch (GuzzleException $e) {
+            throw ApiRequestException::from_exception($e);
+        }
+    }
+}

+ 13 - 5
ot_core/Tests/Unit/Domain/Repository/AbstractApiRepositoryTestCase.php

@@ -5,7 +5,9 @@ namespace Opentalent\OtCore\Tests\Unit\Domain\Repository;
 use GuzzleHttp\Client;
 use Nimut\TestingFramework\TestCase\UnitTestCase;
 use Opentalent\OtCore\Domain\Repository\BaseApiRepository;
+use Opentalent\OtCore\Service\OpentalentApiService;
 use Opentalent\OtCore\Tests\Unit\Fixtures\ApiResponseFixtures;
+use Prophecy\Argument;
 use ReflectionClass;
 use TYPO3\CMS\Core\Core\ApplicationContext;
 
@@ -32,6 +34,8 @@ abstract class AbstractApiRepositoryTestCase extends UnitTestCase
      */
     protected $client;
 
+    protected $service;
+
     public function setUp() {
         // mock the application context
         $this->context = $this->prophesize(ApplicationContext::class);
@@ -40,29 +44,33 @@ abstract class AbstractApiRepositoryTestCase extends UnitTestCase
         $this->context->isTesting()->willReturn(true);
 
         $this->client = $this->prophesize(Client::class);
+        $this->service = new OpentalentApiService(
+            $this->client->reveal(),
+            $this->context->reveal()
+        );
 
         $repositoryClass = new ReflectionClass(static::TESTED_CLASS);
         foreach ($repositoryClass->getMethods() as $method) {
             $method->setAccessible(true);
         }
-        $this->repository = $repositoryClass->newInstanceArgs([$this->client->reveal(), $this->context->reveal()]);
+        $this->repository = $repositoryClass->newInstanceArgs([$this->service]);
 
         $this->fixture = new ApiResponseFixtures();
     }
 
-    protected function injectClientFor($uri) {
+    protected function injectClientFor($uri, $http_method='GET') {
         // mock the Guzzle client
         $willReturn = $this->fixture->get($uri);
         $client = $this->prophesize(Client::class);
-        $client->request(BaseApiRepository::HTTP_METHOD, $uri)
+        $client->request($http_method, $uri)
             ->shouldBeCalled()
             ->willReturn($willReturn);
 
-        $reflectionObject = new \ReflectionObject($this->repository);
+        $reflectionObject = new \ReflectionObject($this->service);
         $reflectionMethod = $reflectionObject->getMethod('injectClient');
         $reflectionMethod->setAccessible(true);
 
-        $reflectionMethod->invokeArgs($this->repository, [$client->reveal()]);
+        $reflectionMethod->invokeArgs($this->service, [$client->reveal()]);
     }
 
     protected function callMemberToObject(array $record) {

+ 2 - 110
ot_core/Tests/Unit/Domain/Repository/BaseApiRepositoryTest.php

@@ -13,10 +13,6 @@ use Opentalent\OtCore\Exception\ApiRequestException;
  */
 class ConcreteBaseApiRepository extends BaseApiRepository {
     protected function memberToObject(array $member) { return $member; }
-    public function injectClient($client) { parent::injectClient($client); }
-    public function getResponse($uri, $params = []): \Psr\Http\Message\ResponseInterface { return parent::getResponse($uri, $params); }
-    public function getBody($uri, $params = []): string { return parent::getBody($uri, $params); }
-    public function getJsonDecoded($uri, $params = []): array { return parent::getJsonDecoded($uri, $params); }
     public function getApiFirstRecord($params = [], $forceUri = null) { return parent::getApiFirstRecord($params, $forceUri); }
     public function getApiRecords($params = [], $forceUri = null): ApiPagedCollection { return parent::getApiRecords($params, $forceUri); }
 }
@@ -25,110 +21,6 @@ class BaseApiRepositoryTest extends AbstractApiRepositoryTestCase
 {
     const TESTED_CLASS = 'Opentalent\OtCore\Tests\Unit\Domain\Repository\ConcreteBaseApiRepository';
 
-    /**
-     * get should build a valid url, send a query and
-     * return a Guzzle response object
-     *
-     * @test
-     */
-    public function get() {
-
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
-        $params = ['itemsPerPage' => 10, 'foo' => 1];
-
-        // uri as it is supposed to be processed by the repo
-        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
-
-        $this->injectClientFor($processed_uri);
-        $actual = $this->repository->getResponse($base_uri, $params);
-
-        $this->assertEquals(200, $actual->getStatusCode());
-    }
-
-    /**
-     * get should build a valid url, send a query and
-     * return a Guzzle response object
-     *
-     * @test
-     */
-    public function getWithNoParams() {
-
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
-        $params = [];
-
-        // uri as it is supposed to be processed by the repo
-        $processed_uri = $base_uri . "?_format=json&itemsPerPage=8";
-
-        $this->injectClientFor($processed_uri);
-        $actual = $this->repository->getResponse($base_uri, $params);
-
-        $this->assertEquals(200, $actual->getStatusCode());
-    }
-
-    /**
-     * get should build a valid url, send a query and
-     * return a Guzzle response object
-     *
-     * @test
-     */
-    public function getInvalidUri()
-    {
-        $base_uri = "a very bad uri";
-        $params = [];
-        $processed_uri = $base_uri . "?_format=json&itemsPerPage=8";
-
-        $client = $this->prophesize(Client::class);
-        $client->request(BaseApiRepository::HTTP_METHOD, $processed_uri)
-            ->shouldBeCalled()
-            ->willThrow(new \GuzzleHttp\Exception\TransferException('error'));
-        $this->inject($this->repository, "client", $client->reveal());
-
-        try {
-            $this->repository->getResponse($base_uri, $params);
-            throw new \AssertionError("An ApiRequestException should have been thrown");
-        } catch (ApiRequestException $e) {
-            $this->assertEquals('error', $e->getMessage());
-        }
-    }
-
-    /**
-     * getBody should return the response body as a string
-     *
-     * @test
-     */
-    public function getBody() {
-
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
-        $params = ['itemsPerPage' => 10, 'foo' => 1];
-
-        // uri as it is supposed to be processed by the repo
-        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
-
-        $this->injectClientFor($processed_uri);
-        $actual = $this->repository->getBody($base_uri, $params);
-
-        $this->assertEquals('{"@context": "/api/contexts/PortailOrganization"}', $actual);
-    }
-
-    /**
-     * getBody should return the response body as an array
-     *
-     * @test
-     */
-    public function getJsonDecoded() {
-
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
-        $params = ['itemsPerPage' => 10, 'foo' => 1];
-
-        // uri as it is supposed to be processed by the repo
-        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
-
-        $this->injectClientFor($processed_uri);
-        $actual = $this->repository->getJsonDecoded($base_uri, $params);
-
-        $this->assertEquals(["@context" => "/api/contexts/PortailOrganization"], $actual);
-    }
-
     /**
      * getApiFirstRecord should return the first member of the api response
      * this member has been processed by the memberToObject method, which does nothing here
@@ -136,7 +28,7 @@ class BaseApiRepositoryTest extends AbstractApiRepositoryTestCase
      * @test
      */
     public function getApiFirstRecord() {
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
+        $base_uri = "api/public/organizations";
         $params = ['filter[where][id]' => 1];
         $processed_uri = $base_uri . "?_format=json&filter%5Bwhere%5D%5Bid%5D=1&page=1&totalItems=1&itemsPerPage=8";
         $this->injectClientFor($processed_uri);
@@ -153,7 +45,7 @@ class BaseApiRepositoryTest extends AbstractApiRepositoryTestCase
      * @test
      */
     public function getApiRecords() {
-        $base_uri = "https://api.opentalent.fr/api/public/organizations";
+        $base_uri = "api/public/organizations";
         $params = ['filter[where][id]' => 1];
         $processed_uri = $base_uri . "?_format=json&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8";
         $this->injectClientFor($processed_uri);

+ 2 - 2
ot_core/Tests/Unit/Domain/Repository/DonorRepositoryTest.php

@@ -18,7 +18,7 @@ class DonorRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/donors?_format=json&organizationId=1&page=1&itemsPerPage=8";
+        $expected_uri = "api/public/donors?_format=json&organizationId=1&page=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByOrganizationId($organization_id);
@@ -34,7 +34,7 @@ class DonorRepositoryTest extends AbstractApiRepositoryTestCase
     public function findParentsByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/donors?_format=json&organizationId=1&parent=1&page=1&itemsPerPage=8";
+        $expected_uri = "api/public/donors?_format=json&organizationId=1&parent=1&page=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findParentsByOrganizationId($organization_id);

+ 8 - 8
ot_core/Tests/Unit/Domain/Repository/EventRepositoryTest.php

@@ -17,7 +17,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findById() {
         $event_id = 2;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&filter%5Bwhere%5D%5Bid%5D=2&page=1&totalItems=1&itemsPerPage=8";
+        $expected_uri = "api/public/events?_format=json&filter%5Bwhere%5D%5Bid%5D=2&page=1&totalItems=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findById($event_id);
@@ -33,7 +33,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&itemsPerPage=8";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByOrganizationId($organization_id);
@@ -49,7 +49,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByOrganizationIdWithParams() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByOrganizationId(
@@ -71,7 +71,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findParentsByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1&itemsPerPage=8";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findParentsByOrganizationId($organization_id);
@@ -87,7 +87,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findParentsByOrganizationIdWithParams() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findParentsByOrganizationId(
@@ -109,7 +109,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findChildrenByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1&itemsPerPage=8";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findChildrenByOrganizationId($organization_id);
@@ -125,7 +125,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function findChildrenByOrganizationIdWithParams() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findChildrenByOrganizationId(
@@ -147,7 +147,7 @@ class EventRepositoryTest extends AbstractApiRepositoryTestCase
     public function searchBy() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8";
+        $expected_uri = "api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->searchBy($organization_id, ['filter[where][id]' => 1]);

+ 1 - 17
ot_core/Tests/Unit/Domain/Repository/MemberRepositoryTest.php

@@ -18,29 +18,13 @@ class MemberRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByOrganizationId() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/members?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200";
+        $expected_uri = "api/public/members?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByOrganizationId($organization_id);
         $this->assertEquals($organization_id, $actual->getMembers()[0]->getOrganizationId());
     }
 
-    /**
-     * findByOrganizationId should return an ApiPagedCollection object containing
-     * the member(s) matching the given organizationId
-     *
-     * @test
-     */
-    public function findByOrganizationIdWithCa() {
-        $organization_id = 1;
-
-        $expected_uri = "https://api.opentalent.fr/api/public/members_ca?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200";
-        $this->injectClientFor($expected_uri);
-
-        $actual = $this->repository->findByOrganizationId($organization_id, true);
-        $this->assertEquals($organization_id, $actual->getMembers()[0]->getOrganizationId());
-    }
-
     /**
      * memberToObject should return null if the given
      * member has not the good type

+ 4 - 4
ot_core/Tests/Unit/Domain/Repository/OrganizationRepositoryTest.php

@@ -17,7 +17,7 @@ class OrganizationRepositoryTest extends AbstractApiRepositoryTestCase
     public function findById() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&page=1&totalItems=1&itemsPerPage=8";
+        $expected_uri = "api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&page=1&totalItems=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findById($organization_id);
@@ -32,7 +32,7 @@ class OrganizationRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByName() {
         $name = 'a name';
 
-        $expected_uri = "https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+name&page=1&totalItems=1&itemsPerPage=8";
+        $expected_uri = "api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+name&page=1&totalItems=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByName($name);
@@ -47,7 +47,7 @@ class OrganizationRepositoryTest extends AbstractApiRepositoryTestCase
     public function findByInexistantName() {
         $name = 'a unknown name';
 
-        $expected_uri = "https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+unknown+name&page=1&totalItems=1&itemsPerPage=8";
+        $expected_uri = "api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+unknown+name&page=1&totalItems=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findByName($name);
@@ -62,7 +62,7 @@ class OrganizationRepositoryTest extends AbstractApiRepositoryTestCase
     public function findChildrenById() {
         $organization_id = 1;
 
-        $expected_uri = "https://api.opentalent.fr/api/public/organizations?_format=json&parentId=1&children=1&page=1&itemsPerPage=8";
+        $expected_uri = "api/public/organizations?_format=json&parentId=1&children=1&page=1&itemsPerPage=8";
         $this->injectClientFor($expected_uri);
 
         $actual = $this->repository->findChildrenById($organization_id);

+ 30 - 30
ot_core/Tests/Unit/Fixtures/ApiResponseFixtures.php

@@ -160,39 +160,39 @@ class ApiResponseFixtures
      * @var string[]
      */
     private $map = [
-        'https://api.opentalent.fr/api/public/organizations?_format=json&itemsPerPage=10&foo=1' => 'stub',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&itemsPerPage=8' => 'stub',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&page=1&totalItems=1&itemsPerPage=8' => 'org',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+name&page=1&totalItems=1&itemsPerPage=8' => 'org',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&parentId=1&children=1&page=1&itemsPerPage=8' => 'org',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8' => 'org',
-        'https://api.opentalent.fr/api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+unknown+name&page=1&totalItems=1&itemsPerPage=8' => 'org',
-        'https://api.opentalent.fr/api/public/events?_format=json&filter%5Bwhere%5D%5Bid%5D=2&page=1&totalItems=1&itemsPerPage=8' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&itemsPerPage=8' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1&itemsPerPage=8' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1&itemsPerPage=8' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1' => 'event',
-        'https://api.opentalent.fr/api/public/events?_format=json&organizationId=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1' => 'event',
-        'https://api.opentalent.fr/api/public/donors?_format=json&organizationId=1&page=1&itemsPerPage=8' => 'donor',
-        'https://api.opentalent.fr/api/public/donors?_format=json&organizationId=1&parent=1&page=1&itemsPerPage=8' => 'donor',
-        'https://api.opentalent.fr/api/public/members?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200' => 'member',
-        'https://api.opentalent.fr/api/public/members_ca?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200' => 'member'
+        'api/public/organizations?_format=json' => 'stub',
+        'api/public/organizations?_format=json&itemsPerPage=10&foo=1' => 'stub',
+        'api/public/organizations?_format=json&itemsPerPage=8' => 'stub',
+        'api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&page=1&totalItems=1&itemsPerPage=8' => 'org',
+        'api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+name&page=1&totalItems=1&itemsPerPage=8' => 'org',
+        'api/public/organizations?_format=json&parentId=1&children=1&page=1&itemsPerPage=8' => 'org',
+        'api/public/organizations?_format=json&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8' => 'org',
+        'api/public/organizations?_format=json&filter%5Bwhere%5D%5Bname%5D=a+unknown+name&page=1&totalItems=1&itemsPerPage=8' => 'org',
+        'api/public/events?_format=json&filter%5Bwhere%5D%5Bid%5D=2&page=1&totalItems=1&itemsPerPage=8' => 'event',
+        'api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&itemsPerPage=8' => 'event',
+        'api/public/events?_format=json&organizationId=1&page=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1&itemsPerPage=8' => 'event',
+        'api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1&itemsPerPage=8' => 'event',
+        'api/public/events?_format=json&organizationId=1&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&filter%5Bwhere%5D%5Bid%5D=1&itemsPerPage=8' => 'event',
+        'api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC' => 'event',
+        'api/public/events?_format=json&organizationId=1&page=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&parent=1' => 'event',
+        'api/public/events?_format=json&organizationId=1&itemsPerPage=1&filter%5Bwhere%5D%5BdatetimeStart%5D%5Bgte%5D=2021-01-01T00%3A00%3A00%2B00%3A00&filter%5Bwhere%5D%5BdatetimeEnd%5D%5Blte%5D=2021-01-31T00%3A00%3A00%2B00%3A00&filter%5Border%5D%5B0%5D%5BdatetimeStart%5D=ASC&children=1' => 'event',
+        'api/public/donors?_format=json&organizationId=1&page=1&itemsPerPage=8' => 'donor',
+        'api/public/donors?_format=json&organizationId=1&parent=1&page=1&itemsPerPage=8' => 'donor',
+        'api/public/members?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200' => 'member',
+        'api/public/members_ca?_format=json&filter%5Bwhere%5D%5BorganizationId%5D=1&itemsPerPage=200' => 'member'
     ];
 
     public function get($url) {
-        if (array_key_exists($url, $this->map)) {
-            $query = $this->map[$url];
-            $response = new HtmlResponse(
-                $this->responses[$query],
-                200,
-                []
-            );
-            return $response;
-        } else {
-            return null;
+        foreach ($this->map as $needle => $response_key) {
+            if (str_ends_with($url, $needle))
+            {
+                return new HtmlResponse(
+                    $this->responses[$response_key],
+                    200,
+                    []
+                );
+            }
         }
+        throw new \RuntimeException("No fixture result for uri " . $url);
     }
-
 }

+ 163 - 0
ot_core/Tests/Unit/Service/OpentalentApiServiceTest.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace Opentalent\OtCore\Tests\Unit\Service;
+
+use GuzzleHttp\Client;
+use Nimut\TestingFramework\TestCase\UnitTestCase;
+use Opentalent\OtCore\Exception\ApiRequestException;
+use Opentalent\OtCore\Service\OpentalentApiService;
+use Opentalent\OtCore\Tests\Unit\Fixtures\ApiResponseFixtures;
+use PHPUnit\TextUI\RuntimeException;
+use Prophecy\Argument;
+use TYPO3\CMS\Core\Core\ApplicationContext;
+
+class OpentalentApiServiceTest extends UnitTestCase
+{
+    protected $context;
+    /**
+     * @var ApiResponseFixtures
+     */
+    protected $fixture;
+    /**
+     * @var \Prophecy\Prophecy\ObjectProphecy
+     */
+    protected $client;
+
+    protected $service;
+
+    public function setUp() {
+        // mock the application context
+        $this->context = $this->prophesize(ApplicationContext::class);
+        $this->context->isProduction()->willReturn(false);
+        $this->context->isDevelopment()->willReturn(false);
+        $this->context->isTesting()->willReturn(true);
+
+        $this->client = $this->prophesize(Client::class);
+
+        $this->service = new OpentalentApiService($this->client->reveal(), $this->context->reveal());
+
+        $this->fixture = new ApiResponseFixtures();
+    }
+
+    protected function injectClientFor($uri, $http_method='GET') {
+        // mock the Guzzle client
+        $client = $this->prophesize(Client::class);
+        try {
+            $willReturn = $this->fixture->get($uri);
+            $client->request($http_method, $uri)
+                ->shouldBeCalled()
+                ->willReturn($willReturn);
+
+        } catch (\RuntimeException $e) {
+            $client->request($http_method, Argument::type('string'))
+                ->shouldBeCalled()
+                ->willThrow(new \GuzzleHttp\Exception\TransferException('error'));
+        }
+
+        $reflectionObject = new \ReflectionObject($this->service);
+        $reflectionMethod = $reflectionObject->getMethod('injectClient');
+        $reflectionMethod->setAccessible(true);
+
+        $reflectionMethod->invokeArgs($this->service, [$client->reveal()]);
+    }
+
+    /**
+     * get should build a valid url, send a query and
+     * return a Guzzle response object
+     *
+     * @test
+     */
+    public function get() {
+
+        $base_uri = "api/public/organizations";
+        $params = ['itemsPerPage' => 10, 'foo' => 1];
+
+        // uri as it is supposed to be processed by the repo
+        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
+
+        $this->injectClientFor($processed_uri);
+        $actual = $this->service->request('GET', $base_uri, $params);
+
+        $this->assertEquals(200, $actual->getStatusCode());
+    }
+
+    /**
+     * get should build a valid url, send a query and
+     * return a Guzzle response object
+     *
+     * @test
+     */
+    public function getWithNoParams() {
+
+        $base_uri = "api/public/organizations";
+        $params = [];
+
+        // uri as it is supposed to be processed by the repo
+        $processed_uri = $base_uri . "?_format=json";
+
+        $this->injectClientFor($processed_uri);
+        $actual = $this->service->request('GET', $base_uri, $params);
+
+        $this->assertEquals(200, $actual->getStatusCode());
+    }
+
+    /**
+     * get should build a valid url, send a query and
+     * return a Guzzle response object
+     *
+     * @test
+     */
+    public function getInvalidUri()
+    {
+        $base_uri = "a very bad uri";
+        $params = [];
+
+        $this->injectClientFor($base_uri);
+
+        try {
+            $res = $this->service->request('GET', $base_uri, $params);
+            throw new \AssertionError("An ApiRequestException should have been thrown");
+        } catch (ApiRequestException $e) {
+            $this->assertEquals('error', $e->getMessage());
+        }
+    }
+
+    /**
+     * getBody should return the response body as a string
+     *
+     * @test
+     */
+    public function getBody() {
+
+        $base_uri = "api/public/organizations";
+        $params = ['itemsPerPage' => 10, 'foo' => 1];
+
+        // uri as it is supposed to be processed by the repo
+        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
+
+        $this->injectClientFor($processed_uri);
+        $actual = $this->service->getBody($base_uri, $params);
+
+        $this->assertEquals('{"@context": "/api/contexts/PortailOrganization"}', $actual);
+    }
+
+    /**
+     * getBody should return the response body as an array
+     *
+     * @test
+     */
+    public function getJsonDecoded() {
+
+        $base_uri = "api/public/organizations";
+        $params = ['itemsPerPage' => 10, 'foo' => 1];
+
+        // uri as it is supposed to be processed by the repo
+        $processed_uri = $base_uri . "?_format=json&itemsPerPage=10&foo=1";
+
+        $this->injectClientFor($processed_uri);
+        $actual = $this->service->getJsonDecoded($base_uri, $params);
+
+        $this->assertEquals(["@context" => "/api/contexts/PortailOrganization"], $actual);
+    }
+
+}

+ 7 - 7
ot_templating/Classes/ViewHelpers/Members/GetAllCaViewHelper.php

@@ -3,8 +3,8 @@
 namespace Opentalent\OtTemplating\ViewHelpers\Members;
 
 use FluidTYPO3\Vhs\Traits\TemplateVariableViewHelperTrait;
+use Opentalent\OtCore\Domain\Repository\MemberCaRepository;
 use Opentalent\OtCore\ViewHelpers\OtAbstractViewHelper;
-use Opentalent\OtCore\Domain\Repository\MemberRepository;
 use Opentalent\OtCore\Exception\ApiRequestException;
 
 /**
@@ -31,10 +31,10 @@ class GetAllCaViewHelper extends OtAbstractViewHelper {
     protected $escapeOutput = false;
 
     /**
-     * @var MemberRepository
+     * @var MemberCaRepository
      *
      */
-    protected MemberRepository $memberRepository;
+    protected MemberCaRepository $memberCaRepository;
 
     /**
      * -- This method is expected by Fluid --
@@ -100,7 +100,7 @@ class GetAllCaViewHelper extends OtAbstractViewHelper {
 
         // Get members of the structure (only CA members)
         try {
-            $collection = $this->memberRepository->findByOrganizationId($organizationId, true);
+            $collection = $this->memberCaRepository->findByOrganizationId($organizationId);
             $members = $collection->getMembers();
         } catch (ApiRequestException $e) {
             $this->logger->error(sprintf('API Error: %s', $e->getMessage()));
@@ -148,10 +148,10 @@ class GetAllCaViewHelper extends OtAbstractViewHelper {
     }
 
     /**
-     * @param MemberRepository $memberRepository
+     * @param MemberCaRepository $memberCaRepository
      */
-    public function injectMemberRepository(MemberRepository $memberRepository)
+    public function injectMemberCaRepository(MemberCaRepository $memberCaRepository)
     {
-        $this->memberRepository = $memberRepository;
+        $this->memberCaRepository = $memberCaRepository;
     }
 }

+ 7 - 6
ot_templating/Classes/ViewHelpers/Members/GetPresidentViewHelper.php

@@ -3,6 +3,7 @@
 namespace Opentalent\OtTemplating\ViewHelpers\Members;
 
 use FluidTYPO3\Vhs\Traits\TemplateVariableViewHelperTrait;
+use Opentalent\OtCore\Domain\Repository\MemberCaRepository;
 use Opentalent\OtCore\ViewHelpers\OtAbstractViewHelper;
 use Opentalent\OtCore\Domain\Repository\MemberRepository;
 use Opentalent\OtCore\Exception\ApiRequestException;
@@ -30,10 +31,10 @@ class GetPresidentViewHelper extends OtAbstractViewHelper {
     protected $escapeOutput = false;
 
     /**
-     * @var MemberRepository
+     * @var MemberCaRepository
      *
      */
-    protected MemberRepository $memberRepository;
+    protected MemberCaRepository $memberCaRepository;
 
     /**
      * -- This method is expected by Fluid --
@@ -69,7 +70,7 @@ class GetPresidentViewHelper extends OtAbstractViewHelper {
 
         // Get members of the structure (only CA members)
         try {
-            $collection = $this->memberRepository->findByOrganizationId($organizationId, true);
+            $collection = $this->memberCaRepository->findByOrganizationId($organizationId);
             $members = $collection->getMembers();
         } catch (ApiRequestException $e) {
             $this->logger->error(sprintf('API Error: %s', $e->getMessage()));
@@ -89,10 +90,10 @@ class GetPresidentViewHelper extends OtAbstractViewHelper {
     }
 
     /**
-     * @param MemberRepository $memberRepository
+     * @param MemberCaRepository $memberCaRepository
      */
-    public function injectMemberRepository(MemberRepository $memberRepository)
+    public function injectMemberCaRepository(MemberCaRepository $memberCaRepository)
     {
-        $this->memberRepository = $memberRepository;
+        $this->memberCaRepository = $memberCaRepository;
     }
 }