Introduced experimental API method and fixture system

Change-Id: Iffb6d3c4f7f21bcf40c904d40d137b206a3ba266
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 50ad7d9..684ef3b 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -17,6 +17,7 @@
 # TODO: Embed collection statistics
 # TODO: Implement tab opener for matches and the tutorial
 # TODO: Implement a "projects" system
+# TODO: Make authentification a plugin
 
 # Start the application and register all routes and plugins
 sub startup {
@@ -97,30 +98,13 @@
 
   # API is not yet set - define
   else {
+
     $kalamar_conf->{api} =
       Mojo::URL->new($kalamar_conf->{api_path})->path('v' . ($kalamar_conf->{api_version} // $API_VERSION) . '/')->to_string;
   };
 
-
-  # Start fixture server for testing purposes
-  if ($self->mode eq 'test') {
-
-    $self->log->info('Mount test server');
-
-    my $mount_point = '/api/v0.1/';
-
-    $self->plugin(Mount => {
-      $mount_point => $self->home->child(
-        'lib/Kalamar/Apps/test_backend.pl'
-      )
-    });
-
-    # Fix api endpoints
-    $kalamar_conf->{api} = $mount_point;
-  }
-
   # Add development path
-  elsif ($self->mode eq 'development') {
+  if ($self->mode eq 'development') {
     push @{$self->static->paths}, 'dev';
   };
 
@@ -213,6 +197,7 @@
 
   # Base query route
   $r->get('/')->to('search#query')->name('index');
+  $r->get('/q2')->to('search2#query');
 
   # Collection route
   $r->get('/corpus')->to('Search#corpus_info')->name('corpus');
diff --git a/lib/Kalamar/Controller/Search2.pm b/lib/Kalamar/Controller/Search2.pm
new file mode 100644
index 0000000..2cdff6c
--- /dev/null
+++ b/lib/Kalamar/Controller/Search2.pm
@@ -0,0 +1,110 @@
+package Kalamar::Controller::Search2;
+use Mojo::Base 'Mojolicious::Controller';
+
+has api => 'http://10.0.10.52:9000/api/';
+
+has no_cache => 0;
+
+has items_per_page => 25;
+
+
+sub query {
+  my $c = shift;
+
+  # Validate user input
+  my $v = $c->validation;
+
+  # In case the user is not known, it is assumed, the user is not logged in
+  my $user = $c->stash('user') // 'not_logged_in';
+
+  $v->optional('q', 'trim');
+  $v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
+  $v->optional('collection'); # Legacy
+  $v->optional('cq', 'trim');
+  # $v->optional('action'); # action 'inspect' is no longer valid
+  $v->optional('snippet');
+  $v->optional('cutoff')->in(qw/true false/);
+  $v->optional('count')->num(1, undef);
+  $v->optional('p');
+  $v->optional('context');
+
+  # Get query
+  my $query = $v->param('q');
+
+  # No query
+  unless ($query) {
+    return $c->render($c->loc('Template_intro', 'intro'));
+  };
+
+  my %query = ();
+  $query{q}       = $v->param('q');
+  $query{ql}      = $v->param('ql');
+  $query{page}    = $v->param('p') if $v->param('p');
+  $query{count}   = $v->param('count') // $c->items_per_page;
+  $query{cq}      = $v->param('cq') // $v->param('collection');
+  $query{cutoff}  = $v->param('cutoff');
+  $query{context} = $v->param('context') // '40-t,40-t'; # 'base/s:p'/'paragraph'
+
+
+  # Create remote request URL
+  my $url = Mojo::URL->new($c->api);
+  $url->query(\%query);
+
+  # Check if total results is cached
+  my $total_results;
+  if (!$c->no_cache) {
+    $total_results = $c->chi->get('total-' . $user . '-' . $url->to_string);
+    $c->stash(total_results => $total_results);
+    $c->app->log->debug('Get total result from cache');
+
+    # Set cutoff unless already set
+    $url->query({cutoff => 'true'});
+  };
+
+
+
+  # Establish 'search_results' taghelper
+  # This is based on Mojolicious::Plugin::Search
+  $c->app->helper(
+    search_results => sub {
+      my $c = shift;
+
+      # This is a tag helper for templates
+      my $cb = shift;
+      if (!ref $cb || !(ref $cb eq 'CODE')) {
+        $c->app->log->error('search_results expects a code block');
+        return '';
+      };
+
+      # Iterate over results
+      my $string = $c->stash('search.results')->map(
+        sub {
+          # Call hit callback
+          $c->stash('search.hit' => $_);
+          local $_ = $_[0];
+          return $cb->($_);
+        })->join;
+
+      # Remove hit from stash
+      delete $c->stash->{'search.hit'};
+      return b($string);
+    }
+  );
+
+
+  return $c->render(
+    template => 'search2',
+    q => $query,
+    ql => scalar $v->param('ql') // 'poliqarp',
+    result_size => 0,
+    start_page => 1,
+    total_pages => 20,
+    total_results => 40,
+    time_exceeded => 0,
+    benchmark => 'Long ...',
+    api_response => ''
+  );
+};
+
+
+1;
diff --git a/t/fixture.t b/t/fixture.t
new file mode 100644
index 0000000..15e4a87
--- /dev/null
+++ b/t/fixture.t
@@ -0,0 +1,36 @@
+use Mojo::Base -strict;
+use Test::More;
+use Test::Mojo;
+use Mojo::File qw/path/;
+
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, 'fixtures');
+
+my $t = Test::Mojo->new($fixtures_path->child('query_backend.pl'));
+
+$t->get_ok('/')
+  ->status_is(200)
+  ->content_is('Query fake server available');
+
+$t->get_ok('/search?ql=cosmas3')
+  ->status_is(400)
+  ->json_is('/errors/0/0',"307")
+  ->json_is('/errors/0/1',"cosmas3 is not a supported query language!")
+  ;
+
+$t->get_ok('/search?q=server_fail')
+  ->status_is(500)
+  ->content_like(qr!Oooops!)
+  ;
+
+$t->get_ok('/search?q=[orth=das&ql=poliqarp')
+  ->status_is(400)
+  ->json_is('/errors/0/0',302)
+  ->json_is('/errors/0/1','Parantheses/brackets unbalanced.')
+  ->json_is('/errors/1/0',302)
+  ->json_is('/errors/1/1','Could not parse query >>> [orth=das <<<.')
+  ;
+
+
+done_testing;
+__END__
diff --git a/t/fixtures/query_backend.pl b/t/fixtures/query_backend.pl
new file mode 100644
index 0000000..a1d3908
--- /dev/null
+++ b/t/fixtures/query_backend.pl
@@ -0,0 +1,243 @@
+#!/usr/bin/env perl
+use Mojolicious::Lite;
+use Mojo::ByteStream 'b';
+use Mojo::Date;
+use Mojo::JSON qw/true false encode_json decode_json/;
+use strict;
+use warnings;
+use Mojo::JWT;
+use Mojo::File;
+
+# This is an API fake server with fixtures
+
+my $secret = 's3cr3t';
+
+helper jwt_encode => sub {
+  shift;
+  return Mojo::JWT->new(
+    secret => $secret,
+    token_type => 'api_token',
+    expires => time + (3 * 34 * 60 * 60),
+    claims => { @_ }
+  );
+};
+
+helper jwt_decode => sub {
+  my ($c, $auth) = @_;
+  $auth =~ s/\s*api_token\s+//;
+  return Mojo::JWT->new(secret => $secret)->decode($auth);
+};
+
+
+# Base page
+get '/' => sub {
+  shift->render(text => 'Query fake server available');
+};
+
+# Search fixtures
+get '/search' => sub {
+  my $c = shift;
+  my $v = $c->validation;
+  $v->optional('q');
+  $v->optional('page');
+  $v->optional('ql');
+  $v->optional('count');
+  $v->optional('context');
+
+  # Response q=x&ql=cosmas3
+  if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
+    return $c->render(
+      status => 400,
+      json => {
+        "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+        "errors" => [[307,"cosmas3 is not a supported query language!"]]
+      });
+  };
+
+  if ($v->param('q')) {
+
+    my $q = $v->param('q');
+
+    # Response q=server_fail
+    if ($q eq 'server_fail') {
+      return $c->render(
+        status => 500,
+        inline => 'Oooops'
+      );
+    }
+
+    # Response q=[orth=das
+    if ($q eq '[orth=das') {
+      return $c->render(
+        status => 400,
+        json => {
+          "\@context" => "http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld",
+          "errors" => [
+            [302,"Parantheses/brackets unbalanced.",0],
+            [302,"Could not parse query >>> [orth=das <<<."]
+          ]
+        }
+      );
+    }
+  };
+
+  my $response = decode_json(
+    Mojo::File->new(__FILE__)->parent->child('response_baum.jsonld')
+    );
+
+
+  # Check authentification
+  if (my $auth = $c->req->headers->header('Authorization')) {
+    if (my $jwt = $c->jwt_decode($auth)) {
+      $response->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
+    };
+  };
+
+  if ($v->param('page')) {
+    $response->{meta}->{startIndex} = $v->param("startIndex");
+  };
+
+
+  # Simple search fixture
+  return $c->render(
+    json => $response
+  );
+};
+
+
+
+############
+# Auth API #
+############
+
+# Request API token
+get '/auth/logout' => sub {
+  my $c = shift;
+
+  if (my $auth = $c->req->headers->header('Authorization')) {
+    if (my $jwt = $c->jwt_decode($auth)) {
+      my $user = $jwt->{username} if $jwt->{username};
+
+      $c->app->log->debug('Server-Logout: ' . $user);
+      return $c->render(json => { msg => [[0, 'Fine!']]});
+    };
+  };
+
+  return $c->render(status => 400, json => { error => [[0, 'No!']]});
+};
+
+
+# Request API token
+get '/auth/apiToken' => sub {
+  my $c = shift;
+
+  # Get auth header
+  my $auth = $c->req->headers->authorization;
+
+  # Authorization missing or not basic
+  if (!$auth || $auth !~ s/\s*Basic\s+//gi) {
+    return $c->render(
+      json => {
+        error => [[2, 'x']]
+      }
+    );
+  };
+
+  # Decode header
+  my ($username, $pwd) = @{b($auth)->b64_decode->split(':')->to_array};
+
+  # the password is 'pass'
+  if ($pwd) {
+
+    # the password is 'pass'
+    if ($pwd eq 'pass') {
+
+      # Render info with token
+      my $jwt = $c->jwt_encode(username => $username);
+
+      # Render in the Kustvakt fashion:
+      return $c->render(
+        format => 'html',
+        text => encode_json({
+          %{$jwt->claims},
+          expires    => $jwt->expires,
+          token      => $jwt->encode,
+          token_type => 'api_token'
+        })
+      );
+    };
+
+    return $c->render(
+      json => {
+        error => [[2004, undef]]
+      }
+    );
+  };
+
+  return $c->render(
+    json => {
+      error => [[2004, undef]]
+    }
+  );
+};
+
+app->start;
+
+
+__END__
+
+
+  # Temporary:
+  my $collection_query = {
+    '@type' => "koral:docGroup",
+    "operation" => "operation:or",
+    "operands" => [
+      {
+	'@type' => "koral:docGroup",
+	"operation" => "operation:and",
+	"operands" => [
+	  {
+	    '@type' => "koral:doc",
+	    "key" => "title",
+	    "match" => "match:eq",
+	    "value" => "Der Birnbaum",
+	    "type" => "type:string"
+	  },
+	  {
+	    '@type' => "koral:doc",
+	    "key" => "pubPlace",
+	    "match" => "match:eq",
+	    "value" => "Mannheim",
+	    "type" => "type:string"
+	  },
+	  {
+	    '@type' => "koral:docGroup",
+	    "operation" => "operation:or",
+	    "operands" => [
+	      {
+		'@type' => "koral:doc",
+		"key" => "subTitle",
+		"match" => "match:eq",
+		"value" => "Aufzucht oder Pflege",
+		"type" => "type:string"
+	      },
+	      {
+		'@type' => "koral:doc",
+		"key" => "subTitle",
+		"match" => "match:eq",
+		"value" => "Gedichte",
+		"type" => "type:string"
+	      }
+	    ]
+	  }
+	]
+      },
+      {
+	'@type' => "koral:doc",
+	"key" => "pubDate",
+	"match" => "match:geq",
+	"value" => "2015-03-05",
+	"type" => "type:date"
+      }
+    ]
+  };
diff --git a/t/fixtures/response_baum.jsonld b/t/fixtures/response_baum.jsonld
new file mode 100644
index 0000000..e401fd1
--- /dev/null
+++ b/t/fixtures/response_baum.jsonld
@@ -0,0 +1,73 @@
+{
+  "@context" : "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
+  "meta" :  {
+    "count" : 25,
+    "startIndex" : 0,
+    "authorized" : undef,
+    "timeout" : 120000,
+    "context" : {
+      "left" : ["token",40],
+      "right" : ["token",40]
+    },
+    "fields" : ["pubDate","subTitle","author","pubPlace","title","textSigle","UID","ID","layerInfos","corpusSigle","docSigle","corpusID","textClass"],
+    "version" : "0.55.7",
+    "benchmark" : "0.120577834 s",
+    "totalResults" : 51,
+    "serialQuery" : "tokens:s:Baum",
+    "itemsPerPage" : 25
+    },
+  "query" : {
+    "@type" : "koral:token",
+    "wrap" : {
+      "@type" : "koral:term",
+      "layer" : "orth",
+      "key" : "Baum",
+      "match" : "match:eq",
+      "foundry" : "opennlp",
+      "rewrites" : [
+        {
+          "@type" : "koral:rewrite",
+          "src" : "Kustvakt",
+          "operation" : "operation:injection",
+          "scope" : "foundry"
+        }
+      ]
+      }
+  },
+  "matches" : [
+    {
+      "field" : "tokens",
+        "pubPlace" : "München",
+      "textSigle" : "GOE/AGI/00000",
+      "docSigle" : "GOE/AGI",
+      "corpusSigle" : "GOE",
+      "title" : "Italienische Reise",
+      "subTitle" : "Auch ich in Arkadien!",
+      "author" : "Goethe, Johann Wolfgang von",
+      "layerInfos" : "base/s=spans corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels mdp/d=rels opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens tt/s=spans",
+      "startMore" : true,
+      "endMore" : true,
+      "snippet" : "<span class=\"context-left\"><span class=\"more\"></span>sie etwas bedeuten zu wollen und machte mit der Oberlippe eine fatale Miene. ich sprach sehr viel mit ihr durch, sie war überall zu Hause und merkte gut auf die Gegenstände. so fragte sie mich einmal, was das für ein </span><span class=\"match\"><mark>Baum</mark></span><span class=\"context-right\"> sei. es war ein schöner großer Ahorn, der erste, der mir auf der ganzen Reise zu Gesichte kam. den hatte sie doch gleich bemerkt und freute sich, da mehrere nach und nach erschienen, daß sie auch diesen Baum unterscheiden könne<span class=\"more\"></span></span>",
+      "matchID" : "match-GOE/AGI/00000-p2030-2031",
+      "UID" : 0,
+      "pubDate" : "1982"
+    },
+    {
+      "field" : "tokens",
+      "pubPlace" : "München",
+      "textSigle" : "GOE/AGI/00000",
+      "docSigle" : "GOE/AGI",
+      "corpusSigle" : "GOE",
+      "title" : "Italienische Reise",
+      "subTitle" : "Auch ich in Arkadien!",
+      "author" : "Goethe, Johann Wolfgang von",
+      "layerInfos" : "base/s=spans corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels mdp/d=rels opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens tt/s=spans",
+      "startMore" : true,
+      "endMore" : true,
+      "snippet" : "<span class=\"context-left\"><span class=\"more\"></span>für ein Baum sei. es war ein schöner großer Ahorn, der erste, der mir auf der ganzen Reise zu Gesichte kam. den hatte sie doch gleich bemerkt und freute sich, da mehrere nach und nach erschienen, daß sie auch diesen </span><span class=\"match\"><mark>Baum</mark></span><span class=\"context-right\"> unterscheiden könne. sie gehe, sagte sie, nach Bozen auf die Messe, wo ich doch wahrscheinlich auch hinzöge. wenn sie mich dort anträfe, müsse ich ihr einen Jahrmarkt kaufen, welches ich ihr denn auch versprach. dort wollte sie auch ihre neue<span class=\"more\"></span></span>",
+      "matchID" : "match-GOE/AGI/00000-p2068-2069",
+      "UID" : 0,
+      "pubDate" : "1982"
+    }
+  ]
+}
diff --git a/lib/Kalamar/Apps/test_backend.pl b/t/fixtures/test_backend.pl
similarity index 100%
rename from lib/Kalamar/Apps/test_backend.pl
rename to t/fixtures/test_backend.pl
diff --git a/t/intro.t b/t/intro.t
index fd48b43..ed8181a 100644
--- a/t/intro.t
+++ b/t/intro.t
@@ -27,6 +27,7 @@
   override => 1
 });
 
+
 $t->get_ok('/')
   ->status_is(200)
   ->text_is('title', 'KorAP - Corpus Analysis Platform')
@@ -43,11 +44,4 @@
   ->text_is('title', 'KorAP: 404 - Page not found')
   ->text_is('h1 span', 'KorAP: 404 - Page not found');
 
-$t->get_ok('/?q=hui')
-  ->status_is(200)
-  ->text_is('title', 'KorAP: Find »hui« with Poliqarp')
-  ->element_exists('meta[name="DC.title"][content="KorAP: Find »hui« with Poliqarp"]')
-  ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
-  ;
-
 done_testing();
diff --git a/t/query.t b/t/query.t
new file mode 100644
index 0000000..de05a78
--- /dev/null
+++ b/t/query.t
@@ -0,0 +1,18 @@
+use Mojo::Base -strict;
+use Test::Mojo;
+use Test::More;
+
+my $t = Test::Mojo->new('Kalamar');
+
+# Query passed
+$t->get_ok('/q2?q=hui')
+  ->status_is(200)
+  ->text_is('#error','')
+  ->text_is('title', 'KorAP: Find »hui« with Poliqarp')
+  ->element_exists('meta[name="DC.title"][content="KorAP: Find »hui« with Poliqarp"]')
+  ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+  ;
+
+
+
+done_testing;
diff --git a/t/remote.t b/t/remote.t
index ec38696..78539d7 100644
--- a/t/remote.t
+++ b/t/remote.t
@@ -1,11 +1,27 @@
 use Mojo::Base -strict;
-use lib '../lib', 'lib';
+use Mojo::File qw/path/;
 use Test::More;
 use Test::Mojo;
 
-$ENV{MOJO_MODE} = 'test';
+my $mount_point = '/api/';
+$ENV{KALAMAR_API} = $mount_point;
 
 my $t = Test::Mojo->new('Kalamar');
+$t->app->defaults('auth_support' => 1);
+
+# Mount fake backend
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, 'fixtures');
+my $fake_backend = $t->app->plugin(
+  Mount => {
+    $mount_point =>
+      $fixtures_path->child('query_backend.pl')
+  }
+);
+
+# Configure fake backend
+$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
 
 if (0) {
 
@@ -59,6 +75,8 @@
 };
 
 
+
+
 # Check for query error
 $t->get_ok('/?q=[orth=das&ql=poliqarp')
   ->element_exists('.notify-error')
diff --git a/t/remote_user.t b/t/remote_user.t
index 4ac8a8e..aa94bad 100644
--- a/t/remote_user.t
+++ b/t/remote_user.t
@@ -1,14 +1,31 @@
 use Mojo::Base -strict;
-use lib '../lib', 'lib';
 use Test::More;
 use Test::Mojo;
+use Mojo::File qw/path/;
 use Data::Dumper;
 
-$ENV{MOJO_MODE} = 'test';
+my $mount_point = '/api/';
+$ENV{KALAMAR_API} = $mount_point;
 
 my $t = Test::Mojo->new('Kalamar');
+$t->app->defaults('auth_support' => 1);
 
-$t->app->defaults(auth_support => 1);
+# Mount fake backend
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, 'fixtures');
+my $fake_backend = $t->app->plugin(
+  Mount => {
+    $mount_point =>
+      $fixtures_path->child('test_backend.pl')
+  }
+);
+
+# Configure fake backend
+$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
+$t->get_ok('/api')
+  ->status_is(200)
+  ->content_is('Fake server available');
 
 $t->get_ok('/?q=Baum')
   ->status_is(200)
@@ -17,6 +34,7 @@
   ->content_like(qr/\"authorized\"\:null/)
   ;
 
+
 $t->get_ok('/')
   ->element_exists('form[action=/user/login] input[name=handle_or_email]');
 
diff --git a/templates/query2.html.ep b/templates/query2.html.ep
new file mode 100644
index 0000000..fa9980c
--- /dev/null
+++ b/templates/query2.html.ep
@@ -0,0 +1,9 @@
+% use Mojo::JSON 'encode_json';
+
+% if (stash('api_response')) {
+%=  javascript begin
+%   my $kq_hash = stash('api_response');
+%   $kq_hash->{matches} = ["..."];
+  KorAP.koralQuery = <%= b(encode_json($kq_hash))->decode %>;
+%   end
+% };
diff --git a/templates/search2.html.ep b/templates/search2.html.ep
new file mode 100644
index 0000000..6ed9c9e
--- /dev/null
+++ b/templates/search2.html.ep
@@ -0,0 +1,32 @@
+% layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
+
+<div id="resultinfo" <% if (stash('results_size')) { %> class="found"<%} %>>
+  <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
+% my $found = stash('total_results');
+  <p class="found">\
+% if ($found != -1) {
+%   my $found_text = loc('numf', number => $found);
+%   if (stash('time_exceeded')) {
+%     $found_text = '> ' . $found_text;
+%   };
+<span id="total-results"><%= $found_text %></span> <%= loc('matchCount', found => $found) %>\
+%# <% if (stash('benchmark')) { %> (~ <%= stash('benchmark') %>)<% } %>
+% } elsif (stash('start_index') == 0 && stash('result_size') == 0) {
+<span id="total-results">0</span> <%= loc('matchCount', found => $found) %>\
+% };
+</p>
+</div>
+
+%= include 'query2'
+
+<div id="search">
+% if (stash('total_results') != 0 && stash('result_size')) {
+  <ol class="align-left">
+%=  search_results begin
+%=    include 'match', match => $_
+%   end
+  </ol>
+% } elsif (stash('result_size') == 0) {
+<p id="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
+% }
+</div>