From e6a997a96372e3c28b8235ac3ab208434a38747d Mon Sep 17 00:00:00 2001 From: Cleverson Sacramento Date: Mon, 7 Jul 2014 10:24:29 -0300 Subject: [PATCH] Inclusão dos casos de teste --- archetype/html-rest/src/main/resources/archetype-resources/pom.xml | 8 ++++++++ archetype/html-rest/src/main/resources/archetype-resources/src/main/java/business/BookmarkBC.java | 6 ++++++ archetype/html-rest/src/main/resources/archetype-resources/src/main/java/entity/Bookmark.java | 25 +++++++++++++++++++++++++ archetype/html-rest/src/main/resources/archetype-resources/src/main/java/persistence/BookmarkDAO.java | 23 +++++++++++++++++++---- archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/AuthREST.java | 7 ------- archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/BookmarkREST.java | 14 ++++++++++++-- archetype/html-rest/src/main/resources/archetype-resources/src/test/java/rest/BookmarkRESTTest.java | 355 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ archetype/html-rest/src/main/resources/archetype-resources/src/test/resources/ValidationMessages.properties | 2 ++ impl/extension/servlet/src/test/java/security/authentication/basic/BasicAuthenticationFilterTest.java | 8 ++++++++ parent/rest/pom.xml | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 10 files changed, 569 insertions(+), 14 deletions(-) create mode 100644 archetype/html-rest/src/main/resources/archetype-resources/src/test/java/rest/BookmarkRESTTest.java create mode 100644 archetype/html-rest/src/main/resources/archetype-resources/src/test/resources/ValidationMessages.properties diff --git a/archetype/html-rest/src/main/resources/archetype-resources/pom.xml b/archetype/html-rest/src/main/resources/archetype-resources/pom.xml index df6b787..d49d243 100755 --- a/archetype/html-rest/src/main/resources/archetype-resources/pom.xml +++ b/archetype/html-rest/src/main/resources/archetype-resources/pom.xml @@ -42,5 +42,13 @@ 1.9.9 provided + + + + org.apache.httpcomponents + httpclient + 4.3.4 + test + diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/business/BookmarkBC.java b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/business/BookmarkBC.java index 9711593..524416b 100644 --- a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/business/BookmarkBC.java +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/business/BookmarkBC.java @@ -1,5 +1,7 @@ package ${package}.business; +import java.util.List; + import ${package}.entity.Bookmark; import ${package}.persistence.BookmarkDAO; import br.gov.frameworkdemoiselle.lifecycle.Startup; @@ -31,4 +33,8 @@ public class BookmarkBC extends DelegateCrud { insert(new Bookmark("Binários", "http://sourceforge.net/projects/demoiselle/files/framework")); } } + + public List find(String filter) { + return getDelegate().find(filter); + } } diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/entity/Bookmark.java b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/entity/Bookmark.java index 49904c9..bd399f2 100644 --- a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/entity/Bookmark.java +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/entity/Bookmark.java @@ -63,4 +63,29 @@ public class Bookmark implements Serializable { public void setLink(String link) { this.link = link; } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Bookmark other = (Bookmark) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } } diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/persistence/BookmarkDAO.java b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/persistence/BookmarkDAO.java index 8ab8128..5f1aa2f 100644 --- a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/persistence/BookmarkDAO.java +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/persistence/BookmarkDAO.java @@ -1,13 +1,28 @@ package ${package}.persistence; -import br.gov.frameworkdemoiselle.stereotype.PersistenceController; -import br.gov.frameworkdemoiselle.template.JPACrud; +import java.util.List; + +import javax.persistence.TypedQuery; import ${package}.entity.Bookmark; +import br.gov.frameworkdemoiselle.stereotype.PersistenceController; +import br.gov.frameworkdemoiselle.template.JPACrud; @PersistenceController public class BookmarkDAO extends JPACrud { - + private static final long serialVersionUID = 1L; - + + public List find(String filter) { + StringBuffer ql = new StringBuffer(); + ql.append(" from Bookmark b "); + ql.append(" where lower(b.description) like :description "); + ql.append(" or lower(b.link) like :link "); + + TypedQuery query = getEntityManager().createQuery(ql.toString(), Bookmark.class); + query.setParameter("description", "%" + filter.toLowerCase() + "%"); + query.setParameter("link", "%" + filter.toLowerCase() + "%"); + + return query.getResultList(); + } } diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/AuthREST.java b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/AuthREST.java index 87f18cb..8d14cd8 100644 --- a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/AuthREST.java +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/AuthREST.java @@ -4,7 +4,6 @@ import javax.inject.Inject; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; @@ -42,12 +41,6 @@ public class AuthREST { return securityContext.getUser(); } - @DELETE - @LoggedIn - public void logout() { - securityContext.logout(); - } - public static class CredentialsForm { @NotNull(message = "{required.field}") diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/BookmarkREST.java b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/BookmarkREST.java index 4122fe2..22f0de0 100644 --- a/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/BookmarkREST.java +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/main/java/rest/BookmarkREST.java @@ -12,6 +12,7 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -22,6 +23,7 @@ import br.gov.frameworkdemoiselle.BadRequestException; import br.gov.frameworkdemoiselle.NotFoundException; import br.gov.frameworkdemoiselle.security.LoggedIn; import br.gov.frameworkdemoiselle.transaction.Transactional; +import br.gov.frameworkdemoiselle.util.Strings; import br.gov.frameworkdemoiselle.util.ValidatePayload; @Path("bookmark") @@ -32,8 +34,16 @@ public class BookmarkREST { @GET @Produces("application/json") - public List find() throws Exception { - return bc.findAll(); + public List find(@QueryParam("q") String query) throws Exception { + List result; + + if (Strings.isEmpty(query)) { + result = bc.findAll(); + } else { + result = bc.find(query); + } + + return result; } @GET diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/test/java/rest/BookmarkRESTTest.java b/archetype/html-rest/src/main/resources/archetype-resources/src/test/java/rest/BookmarkRESTTest.java new file mode 100644 index 0000000..5cc66a8 --- /dev/null +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/test/java/rest/BookmarkRESTTest.java @@ -0,0 +1,355 @@ +package ${package}.rest; + +import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; +import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; +import static javax.servlet.http.HttpServletResponse.SC_OK; +import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.EntityBuilder; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.TypeReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import ${package}.entity.Bookmark; +import br.gov.frameworkdemoiselle.PreconditionFailedException; + +public class BookmarkRESTTest { + + private HttpHost host; + + private static final String BASIC_CREDENTIALS = "Basic " + Base64.encodeBase64String("admin:admin".getBytes()); + + private CloseableHttpClient client; + + private ObjectMapper mapper; + + @Before + public void before() { + host = new HttpHost("localhost", 8080, "http"); + client = HttpClientBuilder.create().build(); + mapper = new ObjectMapper(); + } + + @After + public void after() throws Exception { + client.close(); + } + + @Test + public void findSuccessful() throws ClientProtocolException, IOException { + HttpGet request; + CloseableHttpResponse response; + + request = new HttpGet("/a15/api/bookmark"); + response = client.execute(host, request); + response.close(); + List listAll = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + assertEquals(SC_OK, response.getStatusLine().getStatusCode()); + + String filter = "po"; + request = new HttpGet("/a15/api/bookmark?q=" + filter); + response = client.execute(host, request); + response.close(); + List filteredList = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + assertEquals(SC_OK, response.getStatusLine().getStatusCode()); + + for (Bookmark bookmark : filteredList) { + assertTrue(bookmark.getDescription().toLowerCase().contains(filter) + || bookmark.getLink().toLowerCase().contains(filter)); + assertTrue(listAll.contains(bookmark)); + } + } + + @Test + public void loadSuccessful() throws Exception { + Long id = parseEntity(createSample().getEntity(), Long.class); + + HttpGet request = new HttpGet("/a15/api/bookmark/" + id); + CloseableHttpResponse response = client.execute(host, request); + response.close(); + assertEquals(SC_OK, response.getStatusLine().getStatusCode()); + + Bookmark bookmark = parseEntity(response.getEntity(), Bookmark.class); + assertEquals(Long.valueOf(id), bookmark.getId()); + assertEquals("Google", bookmark.getDescription()); + assertEquals("http://google.com", bookmark.getLink()); + + destroySample(id); + } + + @Test + public void loadFailed() throws ClientProtocolException, IOException { + HttpGet get = new HttpGet("/a15/api/bookmark/99999999"); + CloseableHttpResponse response = client.execute(host, get); + response.close(); + assertEquals(SC_NOT_FOUND, response.getStatusLine().getStatusCode()); + } + + @Test + public void deleteSuccessful() throws Exception { + Long id = parseEntity(createSample().getEntity(), Long.class); + + HttpDelete request = new HttpDelete("/a15/api/bookmark/" + id); + request.addHeader("Authorization", BASIC_CREDENTIALS); + CloseableHttpResponse response = client.execute(host, request); + response.close(); + assertEquals(SC_NO_CONTENT, response.getStatusLine().getStatusCode()); + } + + @Test + public void deleteFailed() throws Exception { + HttpDelete request; + CloseableHttpResponse response; + + Long id = parseEntity(createSample().getEntity(), Long.class); + request = new HttpDelete("/a15/api/bookmark/" + id); + response = client.execute(host, request); + response.close(); + assertEquals(SC_UNAUTHORIZED, response.getStatusLine().getStatusCode()); + destroySample(id); + + request = new HttpDelete("/a15/api/bookmark/99999999"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_NOT_FOUND, response.getStatusLine().getStatusCode()); + } + + @Test + public void insertSuccessful() throws Exception { + CloseableHttpResponse response = createSample(); + response.close(); + + Long id = parseEntity(response.getEntity(), Long.class); + assertNotNull(id); + + String expectedLocation = host.toString() + "/a15/api/bookmark/" + id; + String returnedLocation = response.getHeaders("Location")[0].getValue(); + assertEquals(expectedLocation, returnedLocation); + + HttpGet request = new HttpGet(returnedLocation); + response = client.execute(host, request); + response.close(); + + destroySample(id); + } + + @Test + public void insertFailed() throws Exception { + HttpPost request; + CloseableHttpResponse response; + Bookmark bookmark; + Set violations; + PreconditionFailedException expected; + + bookmark = new Bookmark(); + bookmark.setDescription("Google"); + bookmark.setLink("http://google.com"); + request = new HttpPost("/a15/api/bookmark"); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + response = client.execute(host, request); + response.close(); + assertEquals(SC_UNAUTHORIZED, response.getStatusLine().getStatusCode()); + + bookmark = new Bookmark(); + request = new HttpPost("/a15/api/bookmark"); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_PRECONDITION_FAILED, response.getStatusLine().getStatusCode()); + violations = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + expected = new PreconditionFailedException(); + expected.addViolation("description", "não pode ser nulo"); + expected.addViolation("link", "não pode ser nulo"); + assertEquals(expected.getViolations(), violations); + + bookmark = new Bookmark(); + bookmark.setDescription("Google"); + bookmark.setLink("http: // google . com"); + request = new HttpPost("/a15/api/bookmark"); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_PRECONDITION_FAILED, response.getStatusLine().getStatusCode()); + violations = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + expected = new PreconditionFailedException().addViolation("link", "formato inválido"); + assertEquals(expected.getViolations(), violations); + + bookmark = new Bookmark(); + bookmark.setId(Long.valueOf(123456789)); + bookmark.setDescription("Test"); + bookmark.setLink("http://test.com"); + request = new HttpPost("/a15/api/bookmark"); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_BAD_REQUEST, response.getStatusLine().getStatusCode()); + } + + @Test + public void updateSuccessful() throws Exception { + HttpRequestBase request; + CloseableHttpResponse response = createSample(); + response.close(); + + Bookmark bookmark = new Bookmark(); + bookmark.setDescription("Google Maps"); + bookmark.setLink("http://maps.google.com"); + + Long id = parseEntity(response.getEntity(), Long.class); + String url = "/a15/api/bookmark/" + id; + + request = new HttpPut(url); + ((HttpPut) request).setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_NO_CONTENT, response.getStatusLine().getStatusCode()); + + request = new HttpGet(url); + response = client.execute(host, request); + response.close(); + Bookmark result = parseEntity(response.getEntity(), Bookmark.class); + assertEquals(id, result.getId()); + assertEquals(bookmark.getDescription(), result.getDescription()); + assertEquals(bookmark.getLink(), result.getLink()); + + destroySample(id); + } + + @Test + public void updateFailed() throws Exception { + HttpPut request; + CloseableHttpResponse response = createSample(); + response.close(); + Long id = parseEntity(response.getEntity(), Long.class); + Bookmark bookmark; + Set violations; + PreconditionFailedException expected; + + bookmark = new Bookmark(); + bookmark.setDescription("Google"); + bookmark.setLink("http://google.com"); + request = new HttpPut("/a15/api/bookmark/" + id); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + response = client.execute(host, request); + response.close(); + assertEquals(SC_UNAUTHORIZED, response.getStatusLine().getStatusCode()); + + bookmark = new Bookmark(); + request = new HttpPut("/a15/api/bookmark/" + id); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_PRECONDITION_FAILED, response.getStatusLine().getStatusCode()); + violations = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + expected = new PreconditionFailedException(); + expected.addViolation("description", "não pode ser nulo"); + expected.addViolation("link", "não pode ser nulo"); + assertEquals(expected.getViolations(), violations); + + bookmark = new Bookmark(); + bookmark.setDescription("Google"); + bookmark.setLink("http: // google . com"); + request = new HttpPut("/a15/api/bookmark/" + id); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_PRECONDITION_FAILED, response.getStatusLine().getStatusCode()); + violations = mapper.readValue(response.getEntity().getContent(), + new TypeReference>() { + }); + expected = new PreconditionFailedException().addViolation("link", "formato inválido"); + assertEquals(expected.getViolations(), violations); + + bookmark = new Bookmark(); + bookmark.setId(Long.valueOf(123456789)); + bookmark.setDescription("Test"); + bookmark.setLink("http://test.com"); + request = new HttpPut("/a15/api/bookmark/" + id); + request.setEntity(createEntity(bookmark)); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + response = client.execute(host, request); + response.close(); + assertEquals(SC_BAD_REQUEST, response.getStatusLine().getStatusCode()); + + destroySample(id); + } + + private CloseableHttpResponse createSample() throws Exception { + Bookmark bookmark = new Bookmark(); + bookmark.setDescription("Google"); + bookmark.setLink("http://google.com"); + + HttpPost request = new HttpPost("/a15/api/bookmark"); + request.setEntity(EntityBuilder.create().setText(mapper.writeValueAsString(bookmark)).build()); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", BASIC_CREDENTIALS); + + CloseableHttpResponse response = client.execute(host, request); + response.close(); + + return response; + } + + private void destroySample(Long id) throws Exception { + HttpDelete request = new HttpDelete("/a15/api/bookmark/" + id); + request.addHeader("Authorization", BASIC_CREDENTIALS); + client.execute(host, request).close(); + } + + private T parseEntity(HttpEntity entity, Class type) throws Exception { + return mapper.readValue(entity.getContent(), type); + } + + private HttpEntity createEntity(Object object) throws Exception { + return EntityBuilder.create().setText(mapper.writeValueAsString(object)).build(); + } +} diff --git a/archetype/html-rest/src/main/resources/archetype-resources/src/test/resources/ValidationMessages.properties b/archetype/html-rest/src/main/resources/archetype-resources/src/test/resources/ValidationMessages.properties new file mode 100644 index 0000000..f59930a --- /dev/null +++ b/archetype/html-rest/src/main/resources/archetype-resources/src/test/resources/ValidationMessages.properties @@ -0,0 +1,2 @@ +required.field=campo obrigat\u00F3rio +invalid.url=formato inv\u00E1lido diff --git a/impl/extension/servlet/src/test/java/security/authentication/basic/BasicAuthenticationFilterTest.java b/impl/extension/servlet/src/test/java/security/authentication/basic/BasicAuthenticationFilterTest.java index a685095..7c9a6be 100644 --- a/impl/extension/servlet/src/test/java/security/authentication/basic/BasicAuthenticationFilterTest.java +++ b/impl/extension/servlet/src/test/java/security/authentication/basic/BasicAuthenticationFilterTest.java @@ -9,9 +9,11 @@ import java.io.IOException; import java.net.URL; import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.jboss.arquillian.container.test.api.Deployment; @@ -63,6 +65,12 @@ public class BasicAuthenticationFilterTest { public void loginFailed() throws ClientProtocolException, IOException { String username = "invalid"; String password = "invalid"; + + + HttpPost x = new HttpPost(); + x.setEntity(null); + + //HttpEntity entity HttpGet get = new HttpGet(deploymentUrl + "/helper"); byte[] encoded = Base64.encodeBase64((username + ":" + password).getBytes()); diff --git a/parent/rest/pom.xml b/parent/rest/pom.xml index d307b63..94b9b98 100755 --- a/parent/rest/pom.xml +++ b/parent/rest/pom.xml @@ -34,7 +34,8 @@ ou escreva para a Fundação do Software Livre (FSF) Inc., 51 Franklin St, Fifth Floor, Boston, MA 02111-1301, USA. --> - + 4.0.0 @@ -66,12 +67,44 @@ http://www.serpro.gov.br + + br.gov.frameworkdemoiselle demoiselle-rest compile + + + + + org.jboss.arquillian.junit + arquillian-junit-container + test + + + org.jboss.shrinkwrap.resolver + shrinkwrap-resolver-impl-maven + test + + + org.jboss.arquillian.protocol + arquillian-protocol-servlet + test + @@ -95,14 +128,108 @@ true + javax.ws.rs jsr311-api provided + + + + + + + @@ -129,4 +256,10 @@ + + -- libgit2 0.21.2