From c717ba609d50732d0c9834a316320892b6b27867 Mon Sep 17 00:00:00 2001
From: pbarroca <pbarroca@afi-sa.fr>
Date: Tue, 21 Apr 2015 12:03:41 +0200
Subject: [PATCH] rel #21758 : basic json search results

---
 .../opac/controllers/RechercheController.php  |  54 ++-
 library/Class/CodifGenre.php                  |   7 +-
 library/Class/Notice.php                      |  12 +
 library/Class/Notice/JsonVisitor.php          | 152 +++++++++
 library/Class/Notice/Permalink.php            |  77 +++++
 library/Trait/Facetable.php                   |  28 ++
 library/Trait/NoticeVisitor.php               |   7 +-
 library/ZendAfi/View/Helper/UrlNotice.php     |  41 +--
 .../RechercheControllerJsonTest.php           | 309 ++++++++++++++++++
 .../harry_potter_ecole_sorciers.txt           |   1 +
 10 files changed, 635 insertions(+), 53 deletions(-)
 create mode 100644 library/Class/Notice/JsonVisitor.php
 create mode 100644 library/Class/Notice/Permalink.php
 create mode 100644 library/Trait/Facetable.php
 create mode 100644 tests/application/modules/opac/controllers/RechercheControllerJsonTest.php
 create mode 100644 tests/application/modules/opac/controllers/harry_potter_ecole_sorciers.txt

diff --git a/application/modules/opac/controllers/RechercheController.php b/application/modules/opac/controllers/RechercheController.php
index fabb1d1211a..6504478b306 100644
--- a/application/modules/opac/controllers/RechercheController.php
+++ b/application/modules/opac/controllers/RechercheController.php
@@ -25,16 +25,16 @@ class RechercheController extends ZendAfi_Controller_Action {
 	private $preferences;
 
 
-	public function init()	{
-		$this->moteur=Class_MoteurRecherche::getInstance();
-		$this->view->resultat=array();
+	public function init() {
+		$this->moteur = Class_MoteurRecherche::getInstance();
+		$this->view->resultat = [];
 
-		$this->view->statut = $this->_getParam("statut");
+		$this->view->statut = $this->_getParam('statut');
 
-		if($this->view->statut == "reset")
-			$this->view->statut ="saisie";
+		if ($this->view->statut == 'reset')
+			$this->view->statut = 'saisie';
 
-		$this->view->url_retour = BASE_URL."/opac/recherche/simple/";
+		$this->view->url_retour = BASE_URL . '/opac/recherche/simple/';
 		$this->view->url_facette = $this->view->url_retour;
 
 		$this->preferences = $this->_getParam('current_module')['preferences'];
@@ -83,6 +83,11 @@ class RechercheController extends ZendAfi_Controller_Action {
 		$criteres_recherche = new Class_CriteresRecherche();
 		$criteres_recherche->setParams($params);
 
+		if ('json' == $this->_getParam('format', '')) {
+			$this->_renderJsonResult($criteres_recherche);
+			return;
+		}
+
 		$this->getFrontController()->getRouter()->getCurrentRoute()
 				 ->match(str_replace(BASE_URL,
 														 '',
@@ -90,8 +95,9 @@ class RechercheController extends ZendAfi_Controller_Action {
 
 		$this->view->titre = $this->getTitreRechercheSimple($criteres_recherche);
 
-		if ($this->view->statut == "guidee")
+		if ($this->view->statut == 'guidee')
 			$criteres_recherche->updateRubrique('guidee');
+
 		$this->view->current_domain = $criteres_recherche->getCatalogue();
 
 		$this->showDomainBreadcrumbOnDomainBrowsing();
@@ -100,6 +106,34 @@ class RechercheController extends ZendAfi_Controller_Action {
 	}
 
 
+	protected function _renderJsonResult($criteres_recherche) {
+		$datas = ['url' => Class_Url::absolute(),
+							'total' => 0,
+							'page_size' => $this->preferences['liste_nb_par_page'],
+							'page' => $this->_getParam('page', 1),
+							'records' => []];
+
+		$ret = $this->moteur->lancerRecherche($criteres_recherche);
+		if (isset($ret['statut']) && $ret['statut'] == 'erreur') {
+			$this->_helper->json($datas);
+			return;
+		}
+
+		$datas['total'] = $ret['nombre'];
+		$records = [];
+		(new Storm_Model_Collection($this->_getListNotices($ret['req_liste'])))
+			->eachDo(
+							 function($record) use (&$records) {
+								 $json = new Class_Notice_JsonVisitor();
+								 $record->acceptVisitor($json);
+								 $records[] = $json->data();
+							 });
+
+		$datas['records'] = $records;
+		$this->_helper->json($datas);
+	}
+
+
 	protected function showDomainBreadcrumbOnDomainBrowsing() {
 		$id_module = $this->_getParam('id_module');
 		if (($config = Class_Profil::getCurrentProfil()->getLocalModuleAccueilConfig($id_module))
@@ -128,7 +162,7 @@ class RechercheController extends ZendAfi_Controller_Action {
 		if($title = $this->getTitleForPage($criteres_recherche))
 			return $title;
 
-		return $this->view->_("Résultat de la recherche");
+		return $this->view->_('Résultat de la recherche');
 	}
 
 
@@ -146,7 +180,7 @@ class RechercheController extends ZendAfi_Controller_Action {
 			return $titre_from_module;
 
 		if ($this->_request->getParam('rubrique'))
-			return $this->view->_("Recherche guidée");
+			return $this->view->_('Recherche guidée');
 
 		if ($catalogue = $criteres_recherche->getCatalogue())
 			return $catalogue->getLibelle();
diff --git a/library/Class/CodifGenre.php b/library/Class/CodifGenre.php
index f98c9c69992..b0906a13520 100644
--- a/library/Class/CodifGenre.php
+++ b/library/Class/CodifGenre.php
@@ -16,11 +16,14 @@
  *
  * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
  * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 
 class Class_CodifGenre extends Storm_Model_Abstract {
-	const CODE_FACETTE='G';
+	use Trait_Facetable;
+
+	const CODE_FACETTE = 'G';
+
   protected $_table_name = 'codif_genre';
   protected $_table_primary = 'id_genre';
 
diff --git a/library/Class/Notice.php b/library/Class/Notice.php
index 444634bc0a5..62b2c18b42c 100644
--- a/library/Class/Notice.php
+++ b/library/Class/Notice.php
@@ -822,6 +822,12 @@ class Class_Notice extends Storm_Model_Abstract {
 	}
 
 
+	public function getVolume() {
+		$data = $this->get_subfield('461','v');
+		return isset($data[0]) ? $data[0] : null;
+	}
+
+
 	public function setTitrePrincipal($titre)	{
 		$this->_notice_unimarc->set_subfield('200', 'a', $titre);
 		return $this;
@@ -1467,6 +1473,7 @@ class Class_Notice extends Storm_Model_Abstract {
 	public function acceptVisitor($visitor) {
 		$indexation = new Class_Indexation();
 		$visitor->visitClefAlpha($this->getClefAlpha());
+		$visitor->visitSerialTitle($this->_getTitresDansZones(['461$t']));
 		$visitor->visitTitres($this->_getTitresDansZones(['200$a', '200$e']));
 		$visitor->visitAuteurs($this->_getAuteursDansZone('700', $indexation));
 		$visitor->visitContributeurs($this->_getAuteursDansZone('702', $indexation));
@@ -1489,6 +1496,11 @@ class Class_Notice extends Storm_Model_Abstract {
 		$visitor->visitAlbum($this->getAlbum());
 		$visitor->visitSource(array_merge($this->get_subfield('801', 'b'),
 				                              $this->get_subfield('852', 'k')));
+		$permalink = new Class_Notice_Permalink();
+		$visitor->visitPermalink($permalink->absoluteFor($this, null, true));
+		$visitor->visitVolume($this->getVolume());
+		$visitor->visitCollections($this->getCollections());
+		$visitor->visitGenres($this->getGenres());
 	}
 
 
diff --git a/library/Class/Notice/JsonVisitor.php b/library/Class/Notice/JsonVisitor.php
new file mode 100644
index 00000000000..c84b9530077
--- /dev/null
+++ b/library/Class/Notice/JsonVisitor.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Notice_JsonVisitor {
+	use Trait_NoticeVisitor;
+
+	const KIND_CODE = 'genre';
+
+	protected $_data;
+
+	public function __construct() {
+		$this->_data = new StdClass();
+	}
+
+
+	public function data() {
+		return $this->_data;
+	}
+
+
+	public function visitClefAlpha($clef) {
+		$this->_data->identifier = $clef;
+	}
+
+
+	public function visitPermalink($link) {
+		$this->_data->permalink = $link;
+	}
+
+
+	public function visitTypeDoc($id) {
+		$type = new StdClass();
+		$type->code = $id;
+		$type->label = ($type_doc = Class_TypeDoc::find($id)) ? $type_doc->getLabel() : '';
+
+		$this->_data->type = $type;
+	}
+
+
+	public function visitSerialTitle($title) {
+		$this->_ensureArray('titles');
+		$this->_data->titles = array_merge($title, $this->_data->titles);
+		$this->_data->serial = array_shift($title);
+	}
+
+
+	public function visitTitres($titles) {
+		$this->_ensureArray('title');
+		$this->_data->titles = array_merge($this->_data->titles, $titles);
+	}
+
+
+	public function visitAuteurs($authors) {
+		$this->_data->creators = $authors;
+	}
+
+
+	public function visitEditeur($publisher) {
+		$this->_data->publishers = [$publisher];
+	}
+
+
+	public function visitAnnee($year) {
+		$this->_data->date = $year;
+	}
+
+
+	public function visitVolume($volume) {
+		if ($volume)
+			$this->_data->volume = $volume;
+	}
+
+
+	public function visitLangues($languages) {
+		if ($language = array_shift($languages))
+			$this->_data->language = $language;
+	}
+
+
+	public function visitCollections($collections) {
+		$this->_data->collections = $collections;
+	}
+
+
+	public function visitGenres($kinds) {
+		if (!$kinds)
+			return;
+
+		$classification = $this->_ensureKindsClassification();
+		foreach($kinds as $kind) {
+			$entry = new StdClass();
+			$entry->code = $kind->asFacet();
+			$entry->label= $kind->getLibelle();
+			$classification->entries[] = $entry;
+		}
+	}
+
+
+	protected function _ensureKindsClassification() {
+		$this->_ensureArray('classifications');
+		if ($classification = $this->_classificationByType(static::KIND_CODE))
+			return $classification;
+
+		$classification = $this->_classificationOf(static::KIND_CODE, 'Genre');
+		$this->_data->classifications[] = $classification;
+		return $classification;
+	}
+
+
+	protected function _classificationOf($type, $label) {
+		$classification = new StdClass();
+		$classification->type = $type;
+		$classification->label= $label;
+		$classification->entries = [];
+		return $classification;
+	}
+
+
+	protected function _classificationByType($type) {
+		$this->_ensureArray('classifications');
+		return (new Storm_Collection($this->_data->classifications))
+			->detect(function($item) use ($type) { return $type == $item->type; });
+	}
+
+
+	protected function _ensureArray($name) {
+		if (isset($this->_data->$name))
+			return $this;
+
+		$this->_data->$name = [];
+		return $this;
+	}
+}
\ No newline at end of file
diff --git a/library/Class/Notice/Permalink.php b/library/Class/Notice/Permalink.php
new file mode 100644
index 00000000000..0f902d8c4ef
--- /dev/null
+++ b/library/Class/Notice/Permalink.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+class Class_Notice_Permalink {
+	protected $_special_types = [Class_TypeDoc::ARTICLE,
+															 Class_TypeDoc::RSS,
+															 Class_TypeDoc::SITE,
+															 Class_TypeDoc::CVS];
+
+	public function absoluteFor($record, $name=null, $reset=false, $encode=true) {
+		return Class_Url::absolute($this->paramsFor($record), $name, $reset, $encode);
+	}
+
+
+	public function paramsFor($record) {
+		if (!$record)
+			return ['controller'=>'recherche', 'action' => 'simple'];
+
+		if ($this->_isNormal($record))
+			return ['controller' => 'recherche',
+							'action' => 'viewnotice',
+							'clef' => $record->getClefAlpha(),
+							'id' =>  $record->getId()];
+
+
+		switch($record->getTypeDoc())	{
+			case Class_TypeDoc::ARTICLE:
+				return ['controller' => 'cms',
+								'action' => 'articleview',
+								'id' => $record->getChamp856b()];
+
+			case Class_TypeDoc::RSS:
+				return ['controller' => 'rss',
+								'action' => 'main',
+								'id_flux' => $record->getChamp856b()];
+
+			case Class_TypeDoc::SITE:
+				return ['controller' => 'sito',
+								'action' => 'sitoview',
+								'id_items' => $record->getChamp856b()];
+
+			case Class_TypeDoc::CVS:
+				return ['controller' => 'modules',
+								'action' => 'cvs',
+								'docid' => $record->getId()];
+		}
+	}
+
+
+	protected function _isNormal($record) {
+		return !$this->_isSpecial($record);
+	}
+
+
+	protected function _isSpecial($record) {
+		return in_array($record->getTypeDoc(), $this->_special_types);
+	}
+}
\ No newline at end of file
diff --git a/library/Trait/Facetable.php b/library/Trait/Facetable.php
new file mode 100644
index 00000000000..47ce9562f16
--- /dev/null
+++ b/library/Trait/Facetable.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+
+trait Trait_Facetable {
+	/** @return string */
+	public function asFacet() {
+		return static::CODE_FACETTE . $this->getId();
+	}
+}
\ No newline at end of file
diff --git a/library/Trait/NoticeVisitor.php b/library/Trait/NoticeVisitor.php
index 443126b9a74..596ec40663e 100644
--- a/library/Trait/NoticeVisitor.php
+++ b/library/Trait/NoticeVisitor.php
@@ -16,7 +16,7 @@
  *
  * You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
  * along with BOKEH; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA 
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
  */
 trait Trait_NoticeVisitor {
 	public function visitClefAlpha($clef) {}
@@ -38,6 +38,11 @@ trait Trait_NoticeVisitor {
 	public function visitEan($ean) {}
 	public function visitAlbum($album) {}
 	public function visitSource($source) {}
+	public function visitPermalink($url) {}
+	public function visitSerialTitle($title) {}
+	public function visitVolume($volume) {}
+	public function visitCollections($collections) {}
+	public function visitGenres($genres) {}
 }
 
 ?>
\ No newline at end of file
diff --git a/library/ZendAfi/View/Helper/UrlNotice.php b/library/ZendAfi/View/Helper/UrlNotice.php
index ca5369c0f12..9f3e55c7f05 100644
--- a/library/ZendAfi/View/Helper/UrlNotice.php
+++ b/library/ZendAfi/View/Helper/UrlNotice.php
@@ -31,46 +31,7 @@ class ZendAfi_View_Helper_UrlNotice extends Zend_View_Helper_HtmlElement {
 		if (is_array($notice))
 			$notice = Class_Notice::find($notice['id_notice']);
 
-		$type_doc = $notice->getTypeDoc();
-
-		$options = ['controller' => 'recherche',
-								'action' => 'viewnotice',
-								'clef' => $notice->getClefAlpha(),
-								'id' =>  $notice->getId()];
-
-
-		if (in_array($type_doc, [Class_TypeDoc::ARTICLE,
-														 Class_TypeDoc::RSS,
-														 Class_TypeDoc::SITE,
-														 Class_TypeDoc::CVS]))	{
-			$id_ressource=$notice->getChamp856b();
-			switch($type_doc)	{
-			case Class_TypeDoc::ARTICLE:
-				$options = ['controller' => 'cms',
-										'action' => 'articleview',
-										'id' => $id_ressource];
-				break;
-
-			case Class_TypeDoc::RSS:
-				$options = ['controller' => 'rss',
-										'action' => 'main',
-										'id_flux' => $id_ressource];
-				break;
-
-			case Class_TypeDoc::SITE:
-				$options = ['controller' => 'sito',
-										'action' => 'sitoview',
-										'id_items' => $id_ressource];
-				break;
-
-			case Class_TypeDoc::CVS:
-				$options = ['controller' => 'modules',
-										'action' => 'cvs',
-										'docid' => $notice->getId()];
-				break;
-
-			}
-		}
+		$options = (new Class_Notice_Permalink())->paramsFor($notice);
 
 		$options = array_merge($options,
 													 array_intersect_key(array_filter($preferences),
diff --git a/tests/application/modules/opac/controllers/RechercheControllerJsonTest.php b/tests/application/modules/opac/controllers/RechercheControllerJsonTest.php
new file mode 100644
index 00000000000..64355eec9d1
--- /dev/null
+++ b/tests/application/modules/opac/controllers/RechercheControllerJsonTest.php
@@ -0,0 +1,309 @@
+<?php
+/**
+ * Copyright (c) 2012-2014, Agence Française Informatique (AFI). All rights reserved.
+ *
+ * BOKEH 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.
+ *
+ * There are special exceptions to the terms and conditions of the AGPL as it
+ * is applied to this software (see README file).
+ *
+ * BOKEH 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 BOKEH; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
+ */
+
+abstract class RechercheControllerJsonTestCase extends AbstractControllerTestCase {
+	protected $_storm_default_to_volatile = true;
+	protected $_json;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->_prepareLoader($this->onLoaderOfModel('Class_Notice'));
+		$mock_sql = $this->mock();
+		$this->_prepareSql($mock_sql);
+		Zend_Registry::set('sql', $mock_sql);
+
+		$this->dispatch($this->_searchUrl(), true);
+		$this->_json = json_decode($this->_response->getBody());
+	}
+
+
+	protected function _prepareLoader($loader) {}
+
+	protected function _prepareSql($sql) {}
+
+	protected function _searchUrl() {
+		return 'recherche/simple/expressionRecherche//tri/*/format/json';
+	}
+}
+
+
+
+class RechercheControllerJsonEmptyResultTest extends RechercheControllerJsonTestCase {
+	protected function _prepareLoader($loader) {
+		$loader->whenCalled('findAllByRequeteRecherche')
+					 ->with('Select id_notice from notices', 10, 1)
+					 ->answers([])
+					 ->beStrict();
+	}
+
+
+	protected function _prepareSql($sql) {
+		$sql->whenCalled('fetchOne')
+				->with('Select count(*) from notices')
+				->answers(0)
+
+				->whenCalled('execute')
+				->answers(true)
+			;
+	}
+
+
+	/** @test */
+	public function urlShouldBeCurrentAbsoluteUrl() {
+		$this->assertEquals(Class_Url::absolute(), $this->_json->url);
+	}
+
+
+	/** @test */
+	public function totalShouldBeZero() {
+		$this->assertEquals(0, $this->_json->total);
+	}
+
+
+	/** @test */
+	public function pageSizeShouldBeTen() {
+		$this->assertEquals(10, $this->_json->page_size);
+	}
+
+
+	/** @test */
+	public function pageShouldBeFirstOne() {
+		$this->assertEquals(1, $this->_json->page);
+	}
+
+
+	/** @test */
+	public function recordsShouldBeEmpty() {
+		$this->assertEmpty($this->_json->records);
+	}
+}
+
+
+
+class RechercheControllerJsonOneResultTest extends RechercheControllerJsonTestCase {
+	protected $_record;
+
+	protected function _prepareLoader($loader) {
+		$this->fixture('Class_CodifGenre',
+									 ['id' => 3, 'libelle' => 'Science fiction']);
+
+		$record = $this->fixture('Class_Notice',
+														 ['id' => 234,
+															'clef_alpha' => 'HARRYPOTTERALECOLEDESSORCIERS--ROWLINGJ-1-GALLIMARDJEUNESSE-2011-1',
+															'type_doc' => 1,
+															'unimarc' => file_get_contents(dirname(__FILE__).'/harry_potter_ecole_sorciers.txt'),
+															'annee' => '1998',
+															'facettes' => 'G3']);
+
+		$loader->whenCalled('findAllByRequeteRecherche')
+					 ->with('Select id_notice from notices', 10, 1)
+					 ->answers([$record])
+					 ->beStrict();
+	}
+
+
+	protected function _prepareSql($sql) {
+		$sql->whenCalled('fetchOne')
+				->with('Select count(*) from notices')
+				->answers(1)
+
+				->whenCalled('execute')
+				->answers(true)
+			;
+	}
+
+
+	public function setUp() {
+		parent::setUp();
+		$this->_record = $this->_json->records[0];
+	}
+
+
+	/** @test */
+	public function totalShouldBeOne() {
+		$this->assertEquals(1, $this->_json->total);
+	}
+
+
+	/** @test */
+	public function recordsShouldContainsOneResult() {
+		$this->assertEquals(1, count($this->_json->records));
+	}
+
+
+	/** @test */
+	public function identifierShouldBeAsExpected() {
+		$this->assertEquals('HARRYPOTTERALECOLEDESSORCIERS--ROWLINGJ-1-GALLIMARDJEUNESSE-2011-1',
+												$this->_record->identifier);
+	}
+
+
+	/** @test */
+	public function permalinkShouldBeAsExpected() {
+		$this->assertEquals(Class_Url::absolute(['controller' => 'recherche',
+																						 'action' => 'viewnotice',
+																						 'clef' => 'HARRYPOTTERALECOLEDESSORCIERS--ROWLINGJ-1-GALLIMARDJEUNESSE-2011-1',
+																						 'id' => 234], null, true),
+												$this->_record->permalink);
+	}
+
+
+	/** @test */
+	public function typeCodeShouldBeOne() {
+		$this->assertEquals(1, $this->_record->type->code);
+	}
+
+
+	/** @test */
+	public function typeLabelShouldBeLivres() {
+		$this->assertEquals('Livres', $this->_record->type->label);
+	}
+
+
+	/** @test */
+	public function firstTitleShouldBeHarryPotter() {
+		$this->assertEquals('Harry Potter', $this->_record->titles[0]);
+	}
+
+
+	/** @test */
+	public function secondTitleShouldBeALEcoleDesSorciers() {
+		$this->assertEquals('A l\'école des sorciers', $this->_record->titles[1]);
+	}
+
+
+	/** @test */
+	public function serialTitleShouldBeHarryPotter() {
+		$this->assertEquals('Harry Potter', $this->_record->serial);
+	}
+
+
+	/** @test */
+	public function shouldHave2Creators() {
+		$this->assertEquals(2, count($this->_record->creators));
+	}
+
+
+	/** @test */
+	public function firstCreatorShouldBeJKRowling() {
+		$this->assertEquals('Rowling|Joanne Kathleen', $this->_record->creators[0]);
+	}
+
+
+	/** @test */
+	public function secondCreatorShouldBeJFMenard() {
+		$this->assertEquals('Ménard|Jean-François', $this->_record->creators[1]);
+	}
+
+
+	/** @test */
+	public function firstPublishersShouldBeGallimardJeunesse() {
+		$this->assertEquals('Gallimard Jeunesse (Paris)', $this->_record->publishers[0]);
+	}
+
+
+	/** @test */
+	public function dateShouldBe1998() {
+		$this->assertEquals('1998', $this->_record->date);
+	}
+
+
+	/** @test */
+	public function volumeShouldBeOne() {
+		$this->assertEquals('1', $this->_record->volume);
+	}
+
+
+	/** @test */
+	public function languageShouldNotBePresent() {
+		$this->assertFalse(property_exists($this->_record, 'language'));
+	}
+
+
+	/** @test */
+	public function descriptionShouldNotBePresent() {
+		$this->assertFalse(property_exists($this->_record, 'description'));
+	}
+
+
+	/** @test */
+	public function firstCollectionShouldBeFolioJunior() {
+		$this->assertEquals('Folio junior', $this->_record->collections[0]);
+	}
+
+
+	/** @test */
+	public function shouldContainFirstClassification() {
+		$this->assertNotNull($classif = $this->classifAt(0));
+		return $classif;
+	}
+
+
+	/**
+	 * @test
+	 * @depends shouldContainFirstClassification
+	 */
+	public function firstClassificationTypeShouldBeKind($classif) {
+		$this->assertEquals(Class_Notice_JsonVisitor::KIND_CODE, $classif->type);
+	}
+
+
+	/**
+	 * @test
+	 * @depends shouldContainFirstClassification
+	 */
+	public function firstClassificationLabelShouldBeGenre($classif) {
+		$this->assertEquals('Genre', $classif->label);
+	}
+
+
+	/**
+	 * @test
+	 * @depends shouldContainFirstClassification
+	 */
+	public function firstClassificationShouldContainsEntries($classif) {
+		$this->assertNotEmpty($classif->entries);
+	}
+
+
+	/**
+	 * @test
+	 * @depends shouldContainFirstClassification
+	 */
+	public function firstClassificationEntryCodeShouldBeG3($classif) {
+		$this->assertEquals('G3', $classif->entries[0]->code);
+	}
+
+
+	/**
+	 * @test
+	 * @depends shouldContainFirstClassification
+	 */
+	public function firstClassificationEntryLabelShouldBeScienceFiction($classif) {
+		$this->assertEquals('Science fiction', $classif->entries[0]->label);
+	}
+
+
+	protected function classifAt($position) {
+		return $this->_record->classifications[$position];
+	}
+}
\ No newline at end of file
diff --git a/tests/application/modules/opac/controllers/harry_potter_ecole_sorciers.txt b/tests/application/modules/opac/controllers/harry_potter_ecole_sorciers.txt
new file mode 100644
index 00000000000..9b7fe49af0a
--- /dev/null
+++ b/tests/application/modules/opac/controllers/harry_potter_ecole_sorciers.txt
@@ -0,0 +1 @@
+00654nam2 2200169   450 001000400000010002200004100001300026200002800039210003600067300007300103410002200176461002500198700004100223700005800264801001700322992014500339929  a978-2-07-051842-5  a201502281 aA l'école des sorciers  aPariscGallimard Jeunessed1998  atraduit de : Harry Potter and the philosopher's stone, J. K. Rowling 0tFolio juniorv899  tHarry Potterv10923 1aRowlingbJoanne Kathleen3OPS0089313 1aMénardbJean-François3OPS005699940006Traduit par 2aFrc20150228  uhttp://websvc.pergame.net/afi_opac_services/images/jaquettes/0/0/0/2/thumbs/2387.jpegvhttp://ecx.images-amazon.com/images/I/51KVZWGHASL.jpg
\ No newline at end of file
-- 
GitLab