Browse Source

Merge pull request #819 from pelias/accept_language

negotiate HTTP locales for incoming browser requests
pull/824/head
Peter Johnson 8 years ago committed by GitHub
parent
commit
e8c189e401
  1. 109
      middleware/requestLanguage.js
  2. 2
      package.json
  3. 9
      routes/v1.js
  4. 37
      test/ciao/autocomplete/language_default.coffee
  5. 38
      test/ciao/autocomplete/language_header_invalid.coffee
  6. 38
      test/ciao/autocomplete/language_header_valid.coffee
  7. 37
      test/ciao/autocomplete/language_querystring_invalid.coffee
  8. 37
      test/ciao/autocomplete/language_querystring_valid.coffee
  9. 37
      test/ciao/place/language_default.coffee
  10. 38
      test/ciao/place/language_header_invalid.coffee
  11. 38
      test/ciao/place/language_header_valid.coffee
  12. 37
      test/ciao/place/language_querystring_invalid.coffee
  13. 37
      test/ciao/place/language_querystring_valid.coffee
  14. 37
      test/ciao/reverse/language_default.coffee
  15. 38
      test/ciao/reverse/language_header_invalid.coffee
  16. 38
      test/ciao/reverse/language_header_valid.coffee
  17. 37
      test/ciao/reverse/language_querystring_invalid.coffee
  18. 37
      test/ciao/reverse/language_querystring_valid.coffee
  19. 37
      test/ciao/search/language_default.coffee
  20. 38
      test/ciao/search/language_header_invalid.coffee
  21. 38
      test/ciao/search/language_header_valid.coffee
  22. 37
      test/ciao/search/language_querystring_invalid.coffee
  23. 37
      test/ciao/search/language_querystring_valid.coffee
  24. 322
      test/unit/middleware/requestLanguage.js
  25. 1
      test/unit/run.js

109
middleware/requestLanguage.js

@ -0,0 +1,109 @@
const _ = require('lodash');
const logger = require( 'pelias-logger' ).get( 'api' );
/**
this middleware is responsible for negotiating HTTP locales for incoming
browser requests by reading the querystring param 'lang' or 'Accept-Language' request headers.
the preferred language will then be available on the $req object:
eg. for '?lang=fr' or 'Accept-Language: fr':
```
console.log( req.language );
{
name: 'French',
type: 'living',
scope: 'individual',
iso6393: 'fra',
iso6392B: 'fre',
iso6392T: 'fra',
iso6391: 'fr',
defaulted: false
}
```
for configuration options see:
https://github.com/florrain/locale
**/
const locale = require('locale');
/**
BCP47 language tags can contain three parts:
1. A language subtag (en, zh).
2. A script subtag (Hant, Latn).
3. A region subtag (US, CN).
at time of writing we will only be concerned with 1. (the language subtag) with
the intention of being compatible with the language standard of whosonfirst data.
whosonfirst data is in ISO 639-3 format so we will need to configure the library
to support all ISO 639-1 (2 char) codes and convert them to 639-1 (3-char) codes.
see: https://github.com/whosonfirst/whosonfirst-names
**/
const iso6393 = require('iso-639-3');
// create a dictionary which maps the ISO 639-1 language subtags to a map
// of it's represenation in several different standards.
const language = {};
iso6393.filter( i => !!i.iso6391 ).forEach( i => language[ i.iso6391 ] = i );
// a pre-processed locale list of language subtags we support (all of them).
const allLocales = new locale.Locales( Object.keys( language ) );
// return the middleware
module.exports = function middleware( req, res, next ){
// init an object to store clean (sanitized) input parameters if not initialized
req.clean = req.clean || {};
// init warnings array if not initialized
req.warnings = req.warnings || [];
// set defaults
var lang = language.en;
var locales, best, via = 'default';
// input language via query param
if( via === 'default' && req.query && req.query.lang ){
locales = new locale.Locales( req.query.lang );
best = locales.best( allLocales );
if( best.defaulted ){
req.warnings.push( 'invalid language provided via querystring' );
} else {
lang = language[ best.language ];
via = 'querystring';
}
}
// input language via request headers
if( via === 'default' && req.headers && req.headers['accept-language'] ){
locales = new locale.Locales( req.headers['accept-language'] );
best = locales.best( allLocales );
if( best.defaulted ){
req.warnings.push( 'invalid language provided via header' );
} else {
lang = language[ best.language ];
via = 'header';
}
}
// set $req.language property
req.language = _.clone( lang );
req.language.defaulted = ( via === 'default' );
// set $req.clean property in order to print language info in response header
req.clean.lang = {
name: req.language.name,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
defaulted: req.language.defaulted
};
// logging
logger.info( '[lang] \'%s\' via \'%s\'', lang.iso6391, via );
next();
};

2
package.json

@ -47,8 +47,10 @@
"geojson": "^0.4.0",
"geojson-extent": "^0.3.1",
"geolib": "^2.0.18",
"iso-639-3": "^1.0.0",
"iso3166-1": "^0.3.0",
"joi": "^10.1.0",
"locale": "^0.1.0",
"lodash": "^4.5.0",
"markdown": "0.5.0",
"morgan": "1.8.1",

9
routes/v1.js

@ -18,7 +18,8 @@ var sanitizers = {
/** ----------------------- middleware ------------------------ **/
var middleware = {
calcSize: require('../middleware/sizeCalculator')
calcSize: require('../middleware/sizeCalculator'),
requestLanguage: require('../middleware/requestLanguage')
};
/** ----------------------- controllers ----------------------- **/
@ -108,6 +109,7 @@ function addRoutes(app, peliasConfig) {
]),
search: createRouter([
sanitizers.search.middleware,
middleware.requestLanguage,
middleware.calcSize(),
// 3rd parameter is which query module to use, use fallback/geodisambiguation
// first, then use original search strategy if first query didn't return anything
@ -131,6 +133,7 @@ function addRoutes(app, peliasConfig) {
]),
structured: createRouter([
sanitizers.structured_geocoding.middleware,
middleware.requestLanguage,
middleware.calcSize(),
controllers.search(peliasConfig.api, esclient, queries.structured_geocoding, not(hasResponseDataOrRequestErrors)),
postProc.trimByGranularityStructured(),
@ -150,6 +153,7 @@ function addRoutes(app, peliasConfig) {
]),
autocomplete: createRouter([
sanitizers.autocomplete.middleware,
middleware.requestLanguage,
controllers.search(peliasConfig.api, esclient, queries.autocomplete, not(hasResponseDataOrRequestErrors)),
postProc.distances('focus.point.'),
postProc.confidenceScores(peliasConfig.api),
@ -165,6 +169,7 @@ function addRoutes(app, peliasConfig) {
]),
reverse: createRouter([
sanitizers.reverse.middleware,
middleware.requestLanguage,
middleware.calcSize(),
controllers.coarse_reverse(pipService, coarse_reverse_should_execute),
controllers.search(peliasConfig.api, esclient, queries.reverse, original_reverse_should_execute),
@ -184,6 +189,7 @@ function addRoutes(app, peliasConfig) {
]),
nearby: createRouter([
sanitizers.nearby.middleware,
middleware.requestLanguage,
middleware.calcSize(),
controllers.search(peliasConfig.api, esclient, queries.reverse, not(hasResponseDataOrRequestErrors)),
postProc.distances('point.'),
@ -202,6 +208,7 @@ function addRoutes(app, peliasConfig) {
]),
place: createRouter([
sanitizers.place.middleware,
middleware.requestLanguage,
controllers.place(peliasConfig.api, esclient),
postProc.accuracy(),
postProc.localNamingConventions(),

37
test/ciao/autocomplete/language_default.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/autocomplete?text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/autocomplete/language_header_invalid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/autocomplete?text=example'
headers: 'Accept-Language': 'example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via header' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/autocomplete/language_header_valid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/autocomplete?text=example'
headers: 'Accept-Language': 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/autocomplete/language_querystring_invalid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/autocomplete?lang=example&text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via querystring' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

37
test/ciao/autocomplete/language_querystring_valid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/autocomplete?lang=fr&text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/place/language_default.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/place?ids=geonames:venue:1'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/place/language_header_invalid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/place?ids=geonames:venue:1'
headers: 'Accept-Language': 'example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via header' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/place/language_header_valid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/place?ids=geonames:venue:1'
headers: 'Accept-Language': 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/place/language_querystring_invalid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/place?lang=example&ids=geonames:venue:1'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via querystring' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

37
test/ciao/place/language_querystring_valid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/place?lang=fr&ids=geonames:venue:1'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/reverse/language_default.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/reverse?point.lat=1&point.lon=2'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/reverse/language_header_invalid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/reverse?point.lat=1&point.lon=2'
headers: 'Accept-Language': 'example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via header' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/reverse/language_header_valid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/reverse?point.lat=1&point.lon=2'
headers: 'Accept-Language': 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/reverse/language_querystring_invalid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/reverse?lang=example&point.lat=1&point.lon=2'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via querystring' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

37
test/ciao/reverse/language_querystring_valid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/reverse?lang=fr&point.lat=1&point.lon=2'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/search/language_default.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/search?text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/search/language_header_invalid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/search?text=example'
headers: 'Accept-Language': 'example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via header' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

38
test/ciao/search/language_header_valid.coffee

@ -0,0 +1,38 @@
#> language
path: '/v1/search?text=example'
headers: 'Accept-Language': 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

37
test/ciao/search/language_querystring_invalid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/search?lang=example&text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
json.geocoding.warnings.should.eql [ 'invalid language provided via querystring' ]
#? language
json.geocoding.query['lang'].should.eql {
name: 'English',
iso6391: 'en',
iso6393: 'eng',
defaulted: true
}

37
test/ciao/search/language_querystring_valid.coffee

@ -0,0 +1,37 @@
#> language
path: '/v1/search?lang=fr&text=example'
#? 200 ok
response.statusCode.should.be.equal 200
response.should.have.header 'charset', 'utf8'
response.should.have.header 'content-type', 'application/json; charset=utf-8'
#? valid geocoding block
should.exist json.geocoding
should.exist json.geocoding.version
should.exist json.geocoding.attribution
should.exist json.geocoding.query
should.exist json.geocoding.engine
should.exist json.geocoding.engine.name
should.exist json.geocoding.engine.author
should.exist json.geocoding.engine.version
should.exist json.geocoding.timestamp
#? valid geojson
json.type.should.be.equal 'FeatureCollection'
json.features.should.be.instanceof Array
#? expected errors
should.not.exist json.geocoding.errors
#? expected warnings
should.not.exist json.geocoding.warnings
#? language
json.geocoding.query['lang'].should.eql {
defaulted: false,
iso6391: 'fr',
iso6393: 'fra',
name: 'French'
}

322
test/unit/middleware/requestLanguage.js

@ -0,0 +1,322 @@
var middleware = require('../../../middleware/requestLanguage');
module.exports.tests = {};
var DEFAULTS = {
defaulted: true,
iso6391: 'en',
iso6392B: 'eng',
iso6392T: 'eng',
iso6393: 'eng',
name: 'English',
scope: 'individual',
type: 'living'
};
module.exports.tests.defaults = function(test, common) {
test('default language', function(t) {
var req = {};
middleware(req, {}, function () {
t.deepEqual( req.language, DEFAULTS, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
test('both querystring & header invalid', function(t) {
var req = {
headers: { 'accept-language': 'foobar' },
query: { 'lang': 'foobar' }
};
middleware(req, {}, function () {
t.deepEqual( req.language, DEFAULTS, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, [
'invalid language provided via querystring',
'invalid language provided via header'
]);
t.end();
});
});
};
module.exports.tests.invalid = function(test, common) {
test('headers: invalid language', function(t) {
var req = { headers: {
'accept-language': 'invalid language'
}};
middleware(req, {}, function () {
t.deepEqual( req.language, DEFAULTS, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, [
'invalid language provided via header'
]);
t.end();
});
});
test('query: invalid language', function(t) {
var req = { query: {
lang: 'invalid language'
}};
middleware(req, {}, function () {
t.deepEqual( req.language, DEFAULTS, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, [
'invalid language provided via querystring'
]);
t.end();
});
});
};
module.exports.tests.valid = function(test, common) {
test('headers: valid language - french', function(t) {
var req = { headers: {
'accept-language': 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
}};
var expected = {
defaulted: false,
iso6391: 'fr',
iso6392B: 'fre',
iso6392T: 'fra',
iso6393: 'fra',
name: 'French',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
test('query: valid language - french', function(t) {
var req = { query: {
lang: 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'
}};
var expected = {
defaulted: false,
iso6391: 'fr',
iso6392B: 'fre',
iso6392T: 'fra',
iso6393: 'fra',
name: 'French',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
test('headers: valid language - english', function(t) {
var req = { headers: {
'accept-language': 'en'
}};
var expected = {
defaulted: false,
iso6391: 'en',
iso6392B: 'eng',
iso6392T: 'eng',
iso6393: 'eng',
name: 'English',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
test('query: valid language - english', function(t) {
var req = { query: {
lang: 'en'
}};
var expected = {
defaulted: false,
iso6391: 'en',
iso6392B: 'eng',
iso6392T: 'eng',
iso6393: 'eng',
name: 'English',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
};
module.exports.tests.precedence = function(test, common) {
test('precedence: query has precedence over headers', function(t) {
var req = {
headers: { 'accept-language': 'fr' },
query: { 'lang': 'es' }
};
var expected = {
defaulted: false,
iso6391: 'es',
iso6392B: 'spa',
iso6392T: 'spa',
iso6393: 'spa',
name: 'Spanish',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, []);
t.end();
});
});
test('precedence: invalid querystring but valid header', function(t) {
var req = {
headers: { 'accept-language': 'fr' },
query: { 'lang': 'foobar' }
};
var expected = {
defaulted: false,
iso6391: 'fr',
iso6392B: 'fre',
iso6392T: 'fra',
iso6393: 'fra',
name: 'French',
scope: 'individual',
type: 'living'
};
middleware(req, {}, function () {
t.deepEqual( req.language, expected, '$req.language set' );
t.deepEqual( req.clean.lang, {
defaulted: req.language.defaulted,
iso6391: req.language.iso6391,
iso6393: req.language.iso6393,
name: req.language.name
}, '$req.clean.lang set' );
t.deepEqual( req.warnings, [
'invalid language provided via querystring'
]);
t.end();
});
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('[middleware] requestLanguage: ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

1
test/unit/run.js

@ -39,6 +39,7 @@ var tests = [
require('./middleware/normalizeParentIds'),
require('./middleware/trimByGranularity'),
require('./middleware/trimByGranularityStructured'),
require('./middleware/requestLanguage'),
require('./query/autocomplete'),
require('./query/autocomplete_defaults'),
require('./query/search_defaults'),

Loading…
Cancel
Save