diff --git a/.gitattributes b/.gitattributes
index 3a53f356e..592616cca 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,20 +1,7 @@
-# Check out all text files in UNIX format.
-* text eol=lf
+# Check out all text files in UNIX format, with LF as end of line
+# Don't change this file. If you have any ideas about it, please
+# submit a separate issue about it and we'll discuss.
-# Explicitly declare text files we want to always be normalized and converted
-# to native line endings on checkout.
-*.txt text
-*.java text
-*.groovy text
-*.xml text
-*.md text
-*.pom text
-*.properties text
-*.tex text
-*.vm text
-*.xsl text
-*.yml text
-
-# Denote all files that are truly binary and should not be modified.
-*.png binary
-*.jpg binary
+* text=auto eol=lf
+*.java ident
+*.xml ident
diff --git a/.rultor.yml b/.rultor.yml
index 0d945c069..852e52641 100644
--- a/.rultor.yml
+++ b/.rultor.yml
@@ -42,7 +42,6 @@ merge:
- krzyk
- longtimeago
- pinaf
- - yegor256
deploy:
script: |
version=$(curl -K ../curl-appveyor.cfg --data "{accountName: 'yegor256', projectSlug: 'takes', branch: 'master'}" https://ci.appveyor.com/api/builds | jq -r '.version')
diff --git a/est/takes_09_06_2015.est b/est/takes_09_06_2015.est
new file mode 100644
index 000000000..57e51094b
--- /dev/null
+++ b/est/takes_09_06_2015.est
@@ -0,0 +1,17 @@
+date: 09-06-2015
+author: Igor Khvostenkov
+method: champions.pert
+scope:
+ 1: Design of the interfaces
+ 2: Routing/Dispatching
+ 3: Persistent entities
+ 4: Performance testing
+champions:
+ 1:
+ worst-case: 60
+ best-case: 25
+ most-likely: 35
+ 2:
+ worst-case: 40
+ best-case: 15
+ most-likely: 25
\ No newline at end of file
diff --git a/src/main/java/org/takes/facets/auth/PsBasic.java b/src/main/java/org/takes/facets/auth/PsBasic.java
new file mode 100644
index 000000000..a8575ad6c
--- /dev/null
+++ b/src/main/java/org/takes/facets/auth/PsBasic.java
@@ -0,0 +1,188 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.facets.auth;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.logging.Level;
+import javax.xml.bind.DatatypeConverter;
+import lombok.EqualsAndHashCode;
+import org.takes.Request;
+import org.takes.Response;
+import org.takes.facets.flash.RsFlash;
+import org.takes.facets.forward.RsForward;
+import org.takes.misc.Opt;
+import org.takes.rq.RqHeaders;
+import org.takes.rq.RqHref;
+import org.takes.rs.RsWithHeader;
+
+/**
+ * Pass that checks the user according RFC-2617.
+ *
+ *
The class is immutable and thread-safe.
+ *
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ * @checkstyle ClassDataAbstractionCouplingCheck (250 lines)
+ */
+@EqualsAndHashCode(of = { "entry", "realm" })
+public final class PsBasic implements Pass {
+
+ /**
+ * Authorization response HTTP head.
+ */
+ private static final String AUTH_HEAD = "Basic";
+
+ /**
+ * Entry to validate user information.
+ */
+ private final transient PsBasic.Entry entry;
+
+ /**
+ * Realm.
+ */
+ private final transient String realm;
+
+ /**
+ * Ctor.
+ * @param rlm Realm
+ * @param basic Entry
+ */
+ public PsBasic(final String rlm, final PsBasic.Entry basic) {
+ this.realm = rlm;
+ this.entry = basic;
+ }
+
+ @Override
+ public Opt enter(final Request request) throws IOException {
+ final String decoded = new String(
+ DatatypeConverter.parseBase64Binary(
+ new RqHeaders.Smart(
+ new RqHeaders.Base(request)
+ ).single("authorization").split(AUTH_HEAD)[1]
+ )
+ ).trim();
+ final String user = decoded.split(":")[0];
+ final Opt identity = this.entry.enter(
+ user,
+ decoded.substring(user.length() + 1)
+ );
+ if (!identity.has()) {
+ throw new RsForward(
+ new RsWithHeader(
+ new RsFlash("access denied", Level.WARNING),
+ String.format(
+ "WWW-Authenticate: Basic ream=\"%s\"",
+ this.realm
+ )
+ ),
+ HttpURLConnection.HTTP_UNAUTHORIZED,
+ new RqHref.Base(request).href()
+ );
+ }
+ return identity;
+ }
+
+ @Override
+ public Response exit(final Response response, final Identity identity)
+ throws IOException {
+ return response;
+ }
+
+ /**
+ * Entry interface that is used to check if the received information is
+ * valid.
+ *
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ */
+ public interface Entry {
+
+ /**
+ * Check if is a valid user.
+ * @param user User
+ * @param pwd Password
+ * @return Identity.
+ */
+ Opt enter(String user, String pwd);
+ }
+
+ /**
+ * Fake implementation of {@link PsBasic.Entry}.
+ *
+ * The class is immutable and thread-safe.
+ *
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ *
+ */
+ public static final class Fake implements PsBasic.Entry {
+
+ /**
+ * Should we authenticate a user?
+ */
+ private final transient boolean condition;
+
+ /**
+ * Ctor.
+ * @param cond Condition
+ */
+ public Fake(final boolean cond) {
+ this.condition = cond;
+ }
+
+ @Override
+ public Opt enter(final String usr, final String pwd) {
+ final Opt user;
+ if (this.condition) {
+ user = new Opt.Single(
+ new Identity.Simple(
+ String.format("urn:basic:%s", usr)
+ )
+ );
+ } else {
+ user = new Opt.Empty();
+ }
+ return user;
+ }
+ }
+
+ /**
+ * Empty check.
+ *
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ */
+ public static final class Empty implements PsBasic.Entry {
+
+ @Override
+ public Opt enter(final String user, final String pwd) {
+ return new Opt.Empty();
+ }
+ }
+}
diff --git a/src/main/java/org/takes/facets/auth/social/PsTwitter.java b/src/main/java/org/takes/facets/auth/social/PsTwitter.java
index 2538cc85c..5893f7a5f 100644
--- a/src/main/java/org/takes/facets/auth/social/PsTwitter.java
+++ b/src/main/java/org/takes/facets/auth/social/PsTwitter.java
@@ -28,6 +28,7 @@
import com.jcabi.http.response.RestResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
+import java.net.URI;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.json.JsonObject;
@@ -53,6 +54,12 @@
@EqualsAndHashCode(of = { "app", "key" })
public final class PsTwitter implements Pass {
+ /**
+ * URL for verifying user credentials.
+ */
+ private static final String VERIFY_URL =
+ "https://api.twitter.com/1.1/account/verify_credentials.json";
+
/**
* App name.
*/
@@ -63,58 +70,111 @@ public final class PsTwitter implements Pass {
*/
private final transient String key;
+ /**
+ * Request for fetching app token.
+ */
+ private final transient com.jcabi.http.Request token;
+
+ /**
+ * Request for verifying user credentials.
+ */
+ private final transient com.jcabi.http.Request user;
+
/**
* Ctor.
- * @param gapp Twitter app
- * @param gkey Twitter key
+ * @param name Twitter app
+ * @param keys Twitter key
*/
- public PsTwitter(final String gapp, final String gkey) {
- this.app = gapp;
- this.key = gkey;
+ public PsTwitter(final String name, final String keys) {
+ this(
+ new JdkRequest(
+ new Href("https://api.twitter.com/oauth2/token")
+ .with("grant_type", "client_credentials")
+ .toString()
+ ),
+ new JdkRequest(PsTwitter.VERIFY_URL), name, keys
+ );
+ }
+
+ /**
+ * Ctor with proper requestor for testing purposes.
+ * @param tkn HTTP request for getting token
+ * @param creds HTTP request for verifying credentials
+ * @param name Facebook app
+ * @param keys Facebook key
+ * @checkstyle ParameterNumberCheck (3 lines)
+ */
+ PsTwitter(final com.jcabi.http.Request tkn,
+ final com.jcabi.http.Request creds,
+ final String name,
+ final String keys) {
+ this.token = tkn;
+ this.user = creds;
+ this.app = name;
+ this.key = keys;
}
@Override
public Opt enter(final Request request)
throws IOException {
- return new Opt.Single(PsTwitter.fetch(this.token()));
+ return new Opt.Single(this.identity(this.fetch()));
}
@Override
- public Response exit(final Response response,
- final Identity identity) {
+ public Response exit(final Response response, final Identity identity) {
return response;
}
/**
* Get user name from Twitter, with the token provided.
- * @param token Twitter access token
+ * @param tkn Twitter access token
* @return The user found in Twitter
* @throws IOException If fails
*/
- private static Identity fetch(final String token) throws IOException {
- final String uri = new Href(
- "https://api.twitter.com/1.1/account/verify_credentials.json"
- ).with("access_token", token).toString();
- return PsTwitter.parse(
- new JdkRequest(uri)
+ private Identity identity(final String tkn) throws IOException {
+ return parse(
+ this.user
+ .uri()
+ .set(
+ URI.create(
+ new Href(PsTwitter.VERIFY_URL)
+ .with("access_token", tkn)
+ .toString()
+ )
+ )
+ .back()
.header("accept", "application/json")
.fetch().as(RestResponse.class)
.assertStatus(HttpURLConnection.HTTP_OK)
- .as(JsonResponse.class).json().readObject()
+ .as(JsonResponse.class)
+ .json()
+ .readObject()
+ );
+ }
+
+ /**
+ * Make identity from JSON object.
+ * @param json JSON received from Twitter
+ * @return Identity found
+ */
+ private static Identity parse(final JsonObject json) {
+ final ConcurrentMap props =
+ new ConcurrentHashMap(json.size());
+ props.put("name", json.getString("name"));
+ props.put("picture", json.getString("profile_image_url"));
+ return new Identity.Simple(
+ String.format("urn:twitter:%d", json.getInt("id")),
+ props
);
}
/**
* Retrieve Twitter access token.
- * @return The token
+ * @return The Twitter access token
* @throws IOException If failed
*/
- private String token()
- throws IOException {
- final String uri = new Href("https://api.twitter.com/oauth2/token")
- .with("grant_type", "client_credentials")
- .toString();
- return new JdkRequest(uri)
+ private String fetch() throws IOException {
+ return this.token
.method("POST")
.header(
"Content-Type",
@@ -133,19 +193,4 @@ private String token()
.as(JsonResponse.class)
.json().readObject().getString("access_token");
}
-
- /**
- * Make identity from JSON object.
- * @param json JSON received from Twitter
- * @return Identity found
- */
- private static Identity parse(final JsonObject json) {
- final ConcurrentMap props =
- new ConcurrentHashMap(json.size());
- props.put("name", json.getString("name"));
- props.put("picture", json.getString("profile_image_url"));
- return new Identity.Simple(
- String.format("urn:twitter:%d", json.getInt("id")), props
- );
- }
}
diff --git a/src/main/java/org/takes/rq/RqForm.java b/src/main/java/org/takes/rq/RqForm.java
index 281c39685..170936f06 100644
--- a/src/main/java/org/takes/rq/RqForm.java
+++ b/src/main/java/org/takes/rq/RqForm.java
@@ -37,6 +37,7 @@
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
import lombok.EqualsAndHashCode;
import org.takes.HttpException;
import org.takes.Request;
@@ -68,67 +69,59 @@ public interface RqForm extends Request {
* Get single parameter.
* @param name Parameter name
* @return List of values (can be empty)
+ * @throws IOException if something fails reading parameters
*/
- Iterable param(CharSequence name);
+ Iterable param(CharSequence name) throws IOException;
/**
* Get all parameter names.
* @return All names
+ * @throws IOException if something fails reading parameters
*/
- Iterable names();
+ Iterable names() throws IOException;
/**
* Base implementation of @link RqForm.
* @author Aleksey Popov (alopen@yandex.ru)
* @version $Id$
*/
- @EqualsAndHashCode(callSuper = true, of = "map")
+ @EqualsAndHashCode(callSuper = true, of = "req")
final class Base extends RqWrap implements RqForm {
+
+ /**
+ * Request.
+ */
+ private final transient Request req;
+
/**
- * Map of params and values.
+ * Saved map.
+ * @checkstyle LineLengthCheck (3 lines)
*/
- private final transient ConcurrentMap> map;
+ private final transient List>> saved =
+ new CopyOnWriteArrayList>>();
+
/**
* Ctor.
- * @param req Original request
+ * @param request Original request
* @throws IOException If fails
*/
- @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
- public Base(final Request req) throws IOException {
- super(req);
- final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- new RqPrint(req).printBody(baos);
- final String body = new String(baos.toByteArray());
- this.map = new ConcurrentHashMap>(0);
- for (final String pair : body.split("&")) {
- if (pair.isEmpty()) {
- continue;
- }
- final String[] parts = pair.split("=", 2);
- if (parts.length < 2) {
- throw new HttpException(
- HttpURLConnection.HTTP_BAD_REQUEST,
- String.format("invalid form body pair: %s", pair)
- );
- }
- final String key = RqForm.Base.decode(
- parts[0].trim().toLowerCase(Locale.ENGLISH)
- );
- this.map.putIfAbsent(key, new LinkedList());
- this.map.get(key).add(RqForm.Base.decode(parts[1].trim()));
- }
+ public Base(final Request request) throws IOException {
+ super(request);
+ this.req = request;
}
@Override
- public Iterable param(final CharSequence key) {
+ public Iterable param(
+ final CharSequence key
+ ) throws IOException {
final List values =
- this.map.get(key.toString().toLowerCase(Locale.ENGLISH));
+ this.map().get(key.toString().toLowerCase(Locale.ENGLISH));
final Iterable iter;
if (values == null) {
iter = new VerboseIterable(
Collections.emptyList(),
new Sprintf(
"there are no params \"%s\" among %d others: %s",
- key, this.map.size(), this.map.keySet()
+ key, this.map().size(), this.map().keySet()
)
);
} else {
@@ -143,8 +136,8 @@ public Iterable param(final CharSequence key) {
return iter;
}
@Override
- public Iterable names() {
- return this.map.keySet();
+ public Iterable names() throws IOException {
+ return this.map().keySet();
}
/**
* Decode from URL.
@@ -160,6 +153,44 @@ private static String decode(final CharSequence txt) {
throw new IllegalStateException(ex);
}
}
+ /**
+ * Create map of request parameter.
+ * @return Parameters map or empty map in case of error.
+ * @throws IOException If something fails reading or parsing body
+ */
+ private ConcurrentMap> map() throws IOException {
+ synchronized (this.saved) {
+ if (this.saved.isEmpty()) {
+ final ByteArrayOutputStream
+ baos = new ByteArrayOutputStream();
+ new RqPrint(this.req).printBody(baos);
+ final String body = new String(baos.toByteArray());
+ final ConcurrentMap> map =
+ new ConcurrentHashMap>(1);
+ for (final String pair : body.split("&")) {
+ if (pair.isEmpty()) {
+ continue;
+ }
+ final String[] parts = pair.split("=", 2);
+ if (parts.length < 2) {
+ throw new HttpException(
+ HttpURLConnection.HTTP_BAD_REQUEST,
+ String.format(
+ "invalid form body pair: %s", pair
+ )
+ );
+ }
+ final String key = RqForm.Base.decode(
+ parts[0].trim().toLowerCase(Locale.ENGLISH)
+ );
+ map.putIfAbsent(key, new LinkedList());
+ map.get(key).add(RqForm.Base.decode(parts[1].trim()));
+ }
+ this.saved.add(map);
+ }
+ return this.saved.get(0);
+ }
+ }
}
/**
* Smart decorator, with extra features.
@@ -183,11 +214,12 @@ public Smart(final RqForm req) {
this.origin = req;
}
@Override
- public Iterable param(final CharSequence name) {
+ public Iterable param(final CharSequence name)
+ throws IOException {
return this.origin.param(name);
}
@Override
- public Iterable names() {
+ public Iterable names() throws IOException {
return this.origin.names();
}
@Override
@@ -221,8 +253,10 @@ public String single(final CharSequence name) throws IOException {
* @param name Name of query param
* @param def Default, if not found
* @return Value of it
+ * @throws IOException if something fails reading parameters
*/
- public String single(final CharSequence name, final String def) {
+ public String single(final CharSequence name, final String def)
+ throws IOException {
final String value;
final Iterator params = this.param(name).iterator();
if (params.hasNext()) {
diff --git a/src/main/java/org/takes/rq/RqHeaders.java b/src/main/java/org/takes/rq/RqHeaders.java
index 2e8d1f66a..852449ad0 100644
--- a/src/main/java/org/takes/rq/RqHeaders.java
+++ b/src/main/java/org/takes/rq/RqHeaders.java
@@ -49,6 +49,7 @@
* @version $Id$
* @since 0.1
*/
+@SuppressWarnings("PMD.TooManyMethods")
public interface RqHeaders extends Request {
/**
@@ -210,5 +211,25 @@ public String single(final CharSequence name) throws IOException {
}
return params.next();
}
+ /**
+ * If header is present, returns the first header value.
+ * If not, returns a default value.
+ * @param name Name of header key
+ * @param def Default value
+ * @return Header Value or default value
+ * @throws IOException If fails
+ */
+ public String single(final CharSequence name, final CharSequence def)
+ throws IOException {
+ final String value;
+ final Iterator params = this.header(name).iterator();
+ if (params.hasNext()) {
+ value = params.next();
+ } else {
+ value = def.toString();
+ }
+ return value;
+ }
+
}
}
diff --git a/src/main/java/org/takes/rs/RsJSON.java b/src/main/java/org/takes/rs/RsJSON.java
index b00c1f237..4c563706b 100644
--- a/src/main/java/org/takes/rs/RsJSON.java
+++ b/src/main/java/org/takes/rs/RsJSON.java
@@ -28,6 +28,7 @@
import java.net.HttpURLConnection;
import javax.json.Json;
import javax.json.JsonStructure;
+import javax.json.JsonWriter;
import lombok.EqualsAndHashCode;
import org.takes.Response;
@@ -89,7 +90,12 @@ public RsJSON(final Response res) {
*/
private static byte[] print(final RsJSON.Source src) throws IOException {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
- Json.createWriter(baos).write(src.toJSON());
+ final JsonWriter writer = Json.createWriter(baos);
+ try {
+ writer.write(src.toJSON());
+ } finally {
+ writer.close();
+ }
return baos.toByteArray();
}
diff --git a/src/main/java/org/takes/rs/RsPrettyJSON.java b/src/main/java/org/takes/rs/RsPrettyJSON.java
new file mode 100644
index 000000000..e7cab776a
--- /dev/null
+++ b/src/main/java/org/takes/rs/RsPrettyJSON.java
@@ -0,0 +1,130 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.rs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonWriter;
+import javax.json.stream.JsonGenerator;
+import lombok.EqualsAndHashCode;
+import org.takes.Response;
+
+/**
+ * Response with properly indented JSON body.
+ *
+ * The class is immutable and thread-safe.
+ *
+ * @author Eugene Kondrashev (eugene.kondrashev@gmail.com)
+ * @version $Id$
+ * @since 1.0
+ */
+@EqualsAndHashCode(of = "origin")
+public final class RsPrettyJSON implements Response {
+
+ /**
+ * Original response.
+ */
+ private final transient Response origin;
+
+ /**
+ * Response with properly transformed body.
+ */
+ private final transient List transformed =
+ new CopyOnWriteArrayList();
+
+ /**
+ * Ctor.
+ * @param res Original response
+ */
+ public RsPrettyJSON(final Response res) {
+ this.origin = res;
+ }
+
+ @Override
+ public Iterable head() throws IOException {
+ return this.make().head();
+ }
+
+ @Override
+ public InputStream body() throws IOException {
+ return this.make().body();
+ }
+
+ /**
+ * Make a response.
+ * @return Response just made
+ * @throws IOException If fails
+ */
+ private Response make() throws IOException {
+ synchronized (this.transformed) {
+ if (this.transformed.isEmpty()) {
+ this.transformed.add(
+ new RsWithBody(
+ this.origin,
+ RsPrettyJSON.transform(this.origin.body())
+ )
+ );
+ }
+ }
+ return this.transformed.get(0);
+ }
+
+ /**
+ * Format body with proper indents.
+ * @param body Response body
+ * @return New properly formatted body
+ * @throws IOException If fails
+ */
+ private static byte[] transform(final InputStream body) throws IOException {
+ final ByteArrayOutputStream res = new ByteArrayOutputStream();
+ final JsonReader rdr = Json.createReader(body);
+ try {
+ final JsonObject obj = rdr.readObject();
+ final JsonWriter wrt = Json.createWriterFactory(
+ Collections.singletonMap(
+ JsonGenerator.PRETTY_PRINTING,
+ true
+ )
+ ).createWriter(res);
+ try {
+ wrt.writeObject(obj);
+ } finally {
+ wrt.close();
+ }
+ } catch (final JsonException ex) {
+ throw new IOException(ex);
+ } finally {
+ rdr.close();
+ }
+ return res.toByteArray();
+ }
+}
diff --git a/src/main/java/org/takes/rs/xe/XeStylesheet.java b/src/main/java/org/takes/rs/xe/XeStylesheet.java
index 5def98fb9..b2d0b4670 100644
--- a/src/main/java/org/takes/rs/xe/XeStylesheet.java
+++ b/src/main/java/org/takes/rs/xe/XeStylesheet.java
@@ -35,12 +35,25 @@
* @author Yegor Bugayenko (yegor@teamed.io)
* @version $Id$
* @since 0.1
+ * @link XML+XSLT in a Browser
+ * @link RESTful API and a Web Site in the Same URL
*/
@EqualsAndHashCode(callSuper = true)
public final class XeStylesheet extends XeWrap {
/**
* Ctor.
+ *
+ * The only argument here is a location of XSL stylesheet,
+ * in resources and, at the same time, in HTTP. For example, you
+ * can put "/xsl/main.xsl" there. In the XML output this will generate
+ * an "xml-stylesheet" annotation, which will point the browser
+ * to "http://yoursite/xsl/main.xsl". Obviously, this page must
+ * be available. On the other hand, if you're using RsXSLT, this
+ * file must be available in resources as "/xsl/main.xsl".
+ *
+ *
It is recommended to put XSL files under "src/main/resources/xsl".
+ *
* @param xsl XSL stylesheet
*/
public XeStylesheet(final CharSequence xsl) {
diff --git a/src/main/java/org/takes/tk/TkCORS.java b/src/main/java/org/takes/tk/TkCORS.java
new file mode 100644
index 000000000..9357c24e8
--- /dev/null
+++ b/src/main/java/org/takes/tk/TkCORS.java
@@ -0,0 +1,103 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.tk;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import lombok.EqualsAndHashCode;
+import org.takes.Request;
+import org.takes.Response;
+import org.takes.Take;
+import org.takes.rq.RqHeaders;
+import org.takes.rs.RsWithHeaders;
+import org.takes.rs.RsWithStatus;
+
+/**
+ * CORS take.
+ *
+ *
This take checks if the request (Origin) is allowed to perform
+ * the desired action against the list of the given domains.
+ *
+ * The specification of CORS can be found on the W3C web site on the
+ * following link or even on the RFC-6454 specification.
+ *
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ */
+@EqualsAndHashCode(of = { "origin" , "allowed" })
+public final class TkCORS implements Take {
+
+ /**
+ * Original take.
+ */
+ private final transient Take origin;
+
+ /**
+ * List of allowed domains.
+ */
+ private final transient Set allowed;
+
+ /**
+ * Ctor.
+ * @param take Original
+ * @param domains Allow domains
+ */
+ public TkCORS(final Take take, final String... domains) {
+ this.origin = take;
+ this.allowed = new HashSet(Arrays.asList(domains));
+ }
+
+ @Override
+ public Response act(final Request req) throws IOException {
+ final Response response;
+ final String domain = new RqHeaders.Smart(
+ new RqHeaders.Base(req)
+ ).single("origin", "");
+ if (this.allowed.contains(domain)) {
+ response = new RsWithHeaders(
+ this.origin.act(req),
+ "Access-Control-Allow-Credentials: true",
+ // @checkstyle LineLengthCheck (1 line)
+ "Access-Control-Allow-Methods: OPTIONS, GET, PUT, POST, DELETE, HEAD",
+ String.format(
+ "Access-Control-Allow-Origin: %s",
+ domain
+ )
+ );
+ } else {
+ response = new RsWithHeaders(
+ new RsWithStatus(
+ HttpURLConnection.HTTP_FORBIDDEN
+ ),
+ "Access-Control-Allow-Credentials: false"
+ );
+ }
+ return response;
+ }
+}
diff --git a/src/test/java/org/takes/facets/auth/PsBasicTest.java b/src/test/java/org/takes/facets/auth/PsBasicTest.java
new file mode 100644
index 000000000..50bc3f453
--- /dev/null
+++ b/src/test/java/org/takes/facets/auth/PsBasicTest.java
@@ -0,0 +1,212 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.facets.auth;
+
+import javax.xml.bind.DatatypeConverter;
+import org.apache.commons.lang.RandomStringUtils;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import org.takes.HttpException;
+import org.takes.facets.forward.RsForward;
+import org.takes.misc.Opt;
+import org.takes.rq.RqFake;
+import org.takes.rq.RqMethod;
+import org.takes.rq.RqWithHeaders;
+import org.takes.rs.RsPrint;
+
+/**
+ * Test of {@link PsBasic}.
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ */
+public final class PsBasicTest {
+
+ /**
+ * Basic Auth.
+ */
+ private static final String AUTH_BASIC = "Authorization: Basic %s";
+
+ /**
+ * PsBasic can handle connection with valid credential.
+ * @throws Exception if any error occurs
+ */
+ @Test
+ public void handleConnectionWithValidCredential() throws Exception {
+ final String user = "john";
+ final Opt identity = new PsBasic(
+ "RealmA",
+ new PsBasic.Fake(true)
+ ).enter(
+ new RqWithHeaders(
+ new RqFake(
+ RqMethod.GET,
+ String.format(
+ "?valid_code=%s",
+ // @checkstyle MagicNumberCheck (1 line)
+ RandomStringUtils.randomAlphanumeric(10)
+ )
+ ),
+ this.generateAuthenticateHead(user, "pass")
+ )
+ );
+ MatcherAssert.assertThat(identity.has(), Matchers.is(true));
+ MatcherAssert.assertThat(
+ identity.get().urn(),
+ CoreMatchers.equalTo(this.generateIdentityUrn(user))
+ );
+ }
+
+ /**
+ * PsBasic can handle connection with invalid credential.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void handleConnectionWithInvalidCredential() throws Exception {
+ RsForward forward = new RsForward();
+ try {
+ new PsBasic(
+ "RealmB",
+ new PsBasic.Empty()
+ ).enter(
+ new RqWithHeaders(
+ new RqFake(
+ RqMethod.GET,
+ String.format(
+ "?invalid_code=%s",
+ // @checkstyle MagicNumberCheck (1 line)
+ RandomStringUtils.randomAlphanumeric(10)
+ )
+ ),
+ this.generateAuthenticateHead("username", "wrong")
+ )
+ );
+ } catch (final RsForward ex) {
+ forward = ex;
+ }
+ MatcherAssert.assertThat(
+ new RsPrint(forward).printHead(),
+ Matchers.allOf(
+ Matchers.containsString("HTTP/1.1 401 Unauthorized"),
+ Matchers.containsString(
+ "WWW-Authenticate: Basic ream=\"RealmB\""
+ )
+ )
+ );
+ }
+
+ /**
+ * PsBasic can handle multiple headers with valid credential.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void handleMultipleHeadersWithValidCredential() throws Exception {
+ final String user = "bill";
+ final Opt identity = new PsBasic(
+ "RealmC",
+ new PsBasic.Fake(true)
+ ).enter(
+ new RqWithHeaders(
+ new RqFake(
+ RqMethod.GET,
+ String.format(
+ "?multiple_code=%s",
+ // @checkstyle MagicNumberCheck (1 line)
+ RandomStringUtils.randomAlphanumeric(10)
+ )
+ ),
+ this.generateAuthenticateHead(user, "changeit"),
+ "Referer: http://teamed.io/",
+ "Connection:keep-alive",
+ "Content-Encoding:gzip",
+ "X-Check-Cacheable:YES",
+ "X-Powered-By:Java/1.7"
+ )
+ );
+ MatcherAssert.assertThat(identity.has(), Matchers.is(true));
+ MatcherAssert.assertThat(
+ identity.get().urn(),
+ CoreMatchers.equalTo(this.generateIdentityUrn(user))
+ );
+ }
+
+ /**
+ * PsBasic can handle multiple headers with invalid content.
+ * @throws Exception If some problem inside
+ */
+ @Test(expected = HttpException.class)
+ public void handleMultipleHeadersWithInvalidContent() throws Exception {
+ MatcherAssert.assertThat(
+ new PsBasic(
+ "RealmD",
+ new PsBasic.Fake(true)
+ ).enter(
+ new RqWithHeaders(
+ new RqFake(
+ "XPTO",
+ "/wrong-url"
+ ),
+ String.format(
+ "XYZ%s",
+ this.generateAuthenticateHead("user", "password")
+ ),
+ "XYZReferer: http://teamed.io/",
+ "XYZConnection:keep-alive",
+ "XYZContent-Encoding:gzip",
+ "XYZX-Check-Cacheable:YES",
+ "XYZX-Powered-By:Java/1.7"
+ )
+ ).has()
+ , Matchers.is(false)
+ );
+ }
+
+ /**
+ * Generate the identity urn.
+ * @param user User
+ * @return URN
+ */
+ private String generateIdentityUrn(final String user) {
+ return String.format("urn:basic:%s", user);
+ }
+ /**
+ * Generate the string used on the request that store information about
+ * authentication.
+ * @param user Username
+ * @param pass Password
+ * @return Header string.
+ */
+ private String generateAuthenticateHead(
+ final String user,
+ final String pass
+ ) {
+ final String auth = String.format("%s:%s", user, pass);
+ final String encoded = DatatypeConverter.printBase64Binary(
+ auth.getBytes()
+ );
+ return String.format(AUTH_BASIC, encoded);
+ }
+}
diff --git a/src/test/java/org/takes/facets/auth/social/PsTwitterTest.java b/src/test/java/org/takes/facets/auth/social/PsTwitterTest.java
index ae854a68b..729587cb6 100644
--- a/src/test/java/org/takes/facets/auth/social/PsTwitterTest.java
+++ b/src/test/java/org/takes/facets/auth/social/PsTwitterTest.java
@@ -24,31 +24,79 @@
package org.takes.facets.auth.social;
+import com.jcabi.http.request.FakeRequest;
import java.io.IOException;
-import org.junit.Ignore;
+import java.util.Collections;
+import java.util.Map;
+import javax.json.Json;
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.commons.lang.math.RandomUtils;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
import org.junit.Test;
+import org.takes.facets.auth.Identity;
+import org.takes.facets.auth.Pass;
+import org.takes.rq.RqFake;
/**
* Test case for {@link PsTwitter}.
* @author Prasath Premkumar (popprem@gmail.com)
+ * @author Adam Siemion (adam.siemion@lemonsoftware.pl)
* @version $Id$
* @since 1.0
+ * @checkstyle MagicNumberCheck (500 lines)
+ * @checkstyle MultipleStringLiteralsCheck (500 lines)
*/
public final class PsTwitterTest {
/**
- * Twitter authorization process.
+ * PsTwitter can login.
* @throws IOException If error occurs in the process
- * @todo #11:30min/DEV Test to be implemented for PsTwitter
- * using a oauth mock library (eg:wiremock). Need to modify
- * PsTwitter to accept url from configurations, so that url
- * can be changed for test and real env accordingly.
- * Response to be stubbed for both token and verify_credentials
- * calls and assertions to be performed for the values returned.
*/
- @Ignore
@Test
- public void authorizes() throws IOException {
- throw new UnsupportedOperationException("not implemented yet");
+ public void logsIn() throws IOException {
+ final int tid = RandomUtils.nextInt(1000);
+ final String name = RandomStringUtils.randomAlphanumeric(10);
+ final String picture = RandomStringUtils.randomAlphanumeric(10);
+ final Pass pass = new PsTwitter(
+ new FakeRequest(
+ 200,
+ "HTTP OK",
+ Collections.>emptyList(),
+ String.format(
+ "{\"token_type\":\"bearer\",\"access_token\":\"%s\"}",
+ RandomStringUtils.randomAlphanumeric(10)
+ ).getBytes()
+ ),
+ new FakeRequest(
+ 200,
+ "HTTP OK",
+ Collections.>emptyList(),
+ Json.createObjectBuilder()
+ .add("id", tid)
+ .add("name", name)
+ .add("profile_image_url", picture)
+ .build()
+ .toString()
+ .getBytes()
+ ),
+ RandomStringUtils.randomAlphanumeric(10),
+ RandomStringUtils.randomAlphanumeric(10)
+ );
+ final Identity identity = pass.enter(
+ new RqFake("GET", "")
+ ).get();
+ MatcherAssert.assertThat(
+ identity.urn(),
+ CoreMatchers.equalTo(String.format("urn:twitter:%d", tid))
+ );
+ MatcherAssert.assertThat(
+ identity.properties().get("name"),
+ CoreMatchers.equalTo(name)
+ );
+ MatcherAssert.assertThat(
+ identity.properties().get("picture"),
+ CoreMatchers.equalTo(picture)
+ );
}
}
diff --git a/src/test/java/org/takes/rq/RqFormTest.java b/src/test/java/org/takes/rq/RqFormTest.java
index 5947f4b09..fe7fd67a4 100644
--- a/src/test/java/org/takes/rq/RqFormTest.java
+++ b/src/test/java/org/takes/rq/RqFormTest.java
@@ -64,4 +64,26 @@ public void parsesHttpBody() throws IOException {
);
}
+ /**
+ * Returns always same instances (Cache).
+ * @throws IOException if fails
+ */
+ @Test
+ public void sameInstance() throws IOException {
+ final RqForm req = new RqForm.Base(
+ new RqBuffered(
+ new RqFake(
+ Arrays.asList(
+ "GET /path?a=3",
+ "Host: www.example2.com"
+ ),
+ "alpha=a+b+c&beta=%20No%20"
+ )
+ )
+ );
+ MatcherAssert.assertThat(
+ req.names() == req.names(),
+ Matchers.is(Boolean.TRUE)
+ );
+ }
}
diff --git a/src/test/java/org/takes/rq/RqHeadersTest.java b/src/test/java/org/takes/rq/RqHeadersTest.java
index e76ac0a91..6cced3d30 100644
--- a/src/test/java/org/takes/rq/RqHeadersTest.java
+++ b/src/test/java/org/takes/rq/RqHeadersTest.java
@@ -80,4 +80,48 @@ public void findsAllHeaders() throws IOException {
);
}
+ /**
+ * RqHeaders.Smart can return a single header.
+ * @throws IOException If some problem inside
+ */
+ @Test
+ public void returnsSingleHeader() throws IOException {
+ MatcherAssert.assertThat(
+ new RqHeaders.Smart(
+ new RqHeaders.Base(
+ new RqFake(
+ Arrays.asList(
+ "GET /g",
+ "Host: www.takes.com"
+ ),
+ ""
+ )
+ )
+ ).single("host", "www.takes.net"),
+ Matchers.equalTo("www.takes.com")
+ );
+ }
+ /**
+ * RqHeaders.Smart can return a default header.
+ * @throws IOException If some problem inside
+ */
+ @Test
+ public void returnsDefaultHeader() throws IOException {
+ final String type = "text/plain";
+ MatcherAssert.assertThat(
+ new RqHeaders.Smart(
+ new RqHeaders.Base(
+ new RqFake(
+ Arrays.asList(
+ "GET /f",
+ "Accept: text/json"
+ ),
+ ""
+ )
+ )
+ ).single("Content-type", type),
+ Matchers.equalTo(type)
+ );
+ }
+
}
diff --git a/src/test/java/org/takes/rs/RsJSONTest.java b/src/test/java/org/takes/rs/RsJSONTest.java
index d6d734c98..e8d2a7965 100644
--- a/src/test/java/org/takes/rs/RsJSONTest.java
+++ b/src/test/java/org/takes/rs/RsJSONTest.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import javax.json.Json;
+import javax.json.JsonArrayBuilder;
import javax.json.JsonStructure;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
@@ -56,4 +57,25 @@ public void buildsJsonResponse() throws IOException {
);
}
+ /**
+ * RsJSON can build a big JSON response.
+ * @throws IOException If some problem inside
+ */
+ @Test
+ public void buildsBigJsonResponse() throws IOException {
+ final int size = 100000;
+ final JsonArrayBuilder builder = Json.createArrayBuilder();
+ for (int idx = 0; idx < size; ++idx) {
+ builder.add(
+ Json.createObjectBuilder().add("number", "212 555-1234")
+ );
+ }
+ MatcherAssert.assertThat(
+ Json.createReader(
+ new RsJSON(builder.build()).body()
+ ).readArray().size(),
+ Matchers.equalTo(size)
+ );
+ }
+
}
diff --git a/src/test/java/org/takes/rs/RsPrettyJSONTest.java b/src/test/java/org/takes/rs/RsPrettyJSONTest.java
new file mode 100644
index 000000000..73e7ce4b5
--- /dev/null
+++ b/src/test/java/org/takes/rs/RsPrettyJSONTest.java
@@ -0,0 +1,107 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.rs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+/**
+ * Test case for {@link RsPrettyJSON}.
+ * @author Eugene Kondrashev (eugene.kondrashev@gmail.com)
+ * @version $Id$
+ * @since 1.0
+ */
+public final class RsPrettyJSONTest {
+
+ /**
+ * RsPrettyJSON can format response with JSON body.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void formatsJsonBody() throws Exception {
+ MatcherAssert.assertThat(
+ new RsPrint(
+ new RsPrettyJSON(
+ new RsWithBody("{\"widget\": {\"debug\": \"on\" }}")
+ )
+ ).printBody(),
+ Matchers.is(
+ "\n{\n \"widget\":{\n \"debug\":\"on\"\n }\n}"
+ )
+ );
+ }
+
+ /**
+ * RsPrettyJSON can reject a non-JSON body.
+ * @throws Exception If some problem inside
+ */
+ @Test(expected = IOException.class)
+ public void rejectsNonJsonBody() throws Exception {
+ new RsPrint(new RsPrettyJSON(new RsWithBody("foo"))).printBody();
+ }
+
+ /**
+ * RsPrettyJSON can report correct content length.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void reportsCorrectContentLength() throws Exception {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new RsPrint(
+ new RsWithBody(
+ "\n{\n \"test\":{\n \"test\":\"test\"\n }\n}"
+ )
+ ).printBody(baos);
+ MatcherAssert.assertThat(
+ new RsPrint(
+ new RsPrettyJSON(
+ new RsWithBody("{\"test\": {\"test\": \"test\" }}")
+ )
+ ).printHead(),
+ Matchers.containsString(
+ String.format(
+ "Content-Length: %d",
+ baos.toByteArray().length
+ )
+ )
+ );
+ }
+
+ /**
+ * RsPrettyJSON can conform to equals and hash code contract.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void conformsToEqualsAndHashCode() throws Exception {
+ EqualsVerifier.forClass(RsPrettyJSON.class)
+ .suppress(Warning.TRANSIENT_FIELDS)
+ .withRedefinedSuperclass()
+ .verify();
+ }
+}
diff --git a/src/test/java/org/takes/tk/TkCORSTest.java b/src/test/java/org/takes/tk/TkCORSTest.java
new file mode 100644
index 000000000..98896e25c
--- /dev/null
+++ b/src/test/java/org/takes/tk/TkCORSTest.java
@@ -0,0 +1,112 @@
+/**
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Yegor Bugayenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package org.takes.tk;
+
+import java.net.HttpURLConnection;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import org.takes.facets.hamcrest.HmRsStatus;
+import org.takes.rq.RqFake;
+import org.takes.rq.RqWithHeaders;
+import org.takes.rs.RsPrint;
+import org.takes.rs.RsText;
+
+/**
+ * Test case for {@link TkCORS}.
+ * @author Endrigo Antonini (teamed@endrigo.com.br)
+ * @version $Id$
+ * @since 0.20
+ */
+public final class TkCORSTest {
+
+ /**
+ * TkCORS can handle connections without origin in the request.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void handleConnectionsWithoutOriginInTheRequest() throws Exception {
+ MatcherAssert.assertThat(
+ "It was expected to receive a 403 error.",
+ new TkCORS(
+ new TkFixed(new RsText()),
+ "http://www.netbout.io",
+ "http://www.example.com"
+ ).act(new RqFake()),
+ new HmRsStatus(Matchers.equalTo(HttpURLConnection.HTTP_FORBIDDEN))
+ );
+ }
+
+ /**
+ * TkCORS can handle connections with correct domain on origin.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void handleConnectionsWithCorrectDomainOnOrigin() throws Exception {
+ MatcherAssert.assertThat(
+ "Invalid HTTP status for a request with correct domain.",
+ new TkCORS(
+ new TkFixed(new RsText()),
+ "http://teamed.io",
+ "http://example.com"
+ ).act(
+ new RqWithHeaders(
+ new RqFake(),
+ "Origin: http://teamed.io"
+ )
+ ),
+ new HmRsStatus(Matchers.equalTo(HttpURLConnection.HTTP_OK))
+ );
+ }
+
+ /**
+ * TkCors can't handle connections with wrong domain on origin.
+ * @throws Exception If some problem inside
+ */
+ @Test
+ public void cantHandleConnectionsWithWrongDomainOnOrigin()
+ throws Exception {
+ MatcherAssert.assertThat(
+ "Wrong value on header.",
+ new RsPrint(
+ new TkCORS(
+ new TkFixed(new RsText()),
+ "http://www.teamed.io",
+ "http://sample.com"
+ ).act(
+ new RqWithHeaders(
+ new RqFake(),
+ "Origin: http://wrong.teamed.io"
+ )
+ )
+ ).printHead(),
+ Matchers.allOf(
+ Matchers.containsString("HTTP/1.1 403"),
+ Matchers.containsString(
+ "Access-Control-Allow-Credentials: false"
+ )
+ )
+ );
+ }
+}