diff --git a/docs/backends/rest.md b/docs/backends/rest.md index b281d50..c59a5f4 100644 --- a/docs/backends/rest.md +++ b/docs/backends/rest.md @@ -91,9 +91,13 @@ Content-type: JSON UTF-8 #### Request Body ``` { + "by": "", "search_term": "doe" } ``` +`by` can be: +- `name` +- `threepid` #### Response Body: If users found: @@ -102,8 +106,8 @@ If users found: "limited": false, "results": [ { - "display_name": "John Doe", "avatar_url": "http://domain.tld/path/to/avatar.png", + "display_name": "John Doe", "user_id": "UserIdLocalpart" }, { diff --git a/src/main/java/io/kamax/mxisd/backend/rest/RestDirectoryProvider.java b/src/main/java/io/kamax/mxisd/backend/rest/RestDirectoryProvider.java new file mode 100644 index 0000000..b92abbc --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/rest/RestDirectoryProvider.java @@ -0,0 +1,82 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.backend.rest; + +import io.kamax.matrix.MatrixID; +import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.rest.RestBackendConfig; +import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchRequest; +import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult; +import io.kamax.mxisd.directory.IDirectoryProvider; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.util.RestClientUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class RestDirectoryProvider extends RestProvider implements IDirectoryProvider { + + private MatrixConfig mxCfg; + + public RestDirectoryProvider(RestBackendConfig cfg, MatrixConfig mxCfg) { + super(cfg); + this.mxCfg = mxCfg; + } + + @Override + public boolean isEnabled() { + return cfg.isEnabled() && StringUtils.isNotBlank(cfg.getEndpoints().getDirectory()); + } + + private UserDirectorySearchResult search(String by, String query) { + UserDirectorySearchRequest request = new UserDirectorySearchRequest(query); + request.setBy(by); + try (CloseableHttpResponse httpResponse = client.execute(RestClientUtils.post(cfg.getEndpoints().getDirectory(), request))) { + int status = httpResponse.getStatusLine().getStatusCode(); + if (status < 200 || status >= 300) { + throw new InternalServerError("REST backend: Error: " + IOUtils.toString(httpResponse.getEntity().getContent(), StandardCharsets.UTF_8)); + } + + UserDirectorySearchResult response = parser.parse(httpResponse, UserDirectorySearchResult.class); + for (UserDirectorySearchResult.Result result : response.getResults()) { + result.setUserId(new MatrixID(result.getUserId(), mxCfg.getDomain()).getId()); + } + + return response; + } catch (IOException e) { + throw new InternalServerError("REST backend: I/O error: " + e.getMessage()); + } + } + + @Override + public UserDirectorySearchResult searchByDisplayName(String query) { + return search("name", query); + } + + @Override + public UserDirectorySearchResult searchBy3pid(String query) { + return search("threepid", query); + } + +} diff --git a/src/main/java/io/kamax/mxisd/controller/directory/v1/io/UserDirectorySearchRequest.java b/src/main/java/io/kamax/mxisd/controller/directory/v1/io/UserDirectorySearchRequest.java index 83e5639..1bd333a 100644 --- a/src/main/java/io/kamax/mxisd/controller/directory/v1/io/UserDirectorySearchRequest.java +++ b/src/main/java/io/kamax/mxisd/controller/directory/v1/io/UserDirectorySearchRequest.java @@ -22,12 +22,21 @@ package io.kamax.mxisd.controller.directory.v1.io; public class UserDirectorySearchRequest { + private String by; private String searchTerm; public UserDirectorySearchRequest(String searchTerm) { setSearchTerm(searchTerm); } + public String getBy() { + return by; + } + + public void setBy(String by) { + this.by = by; + } + public String getSearchTerm() { return searchTerm; } diff --git a/src/test/java/io/kamax/mxisd/backend/rest/RestDirectoryProviderTest.java b/src/test/java/io/kamax/mxisd/backend/rest/RestDirectoryProviderTest.java new file mode 100644 index 0000000..a60bc92 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/backend/rest/RestDirectoryProviderTest.java @@ -0,0 +1,167 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.backend.rest; + +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import io.kamax.matrix.MatrixID; +import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.config.rest.RestBackendConfig; +import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class RestDirectoryProviderTest { + + @Rule + public WireMockRule wireMockRule = new WireMockRule(65000); + + private RestDirectoryProvider p; + + private String domain = "example.org"; + private String endpoint = "/directory/search"; + private String byNameSearch = "doe"; + private String byNameAvatar = "http://domain.tld/path/to/avatar.png"; + private String byNameDisplay = "John Doe"; + private String byNameId = "john.doe"; + private String byNameRequest = "{\"by\":\"name\",\"search_term\":\"" + byNameSearch + "\"}"; + private String byNameResponse = "{\"limited\":false,\"results\":[{\"avatar_url\":\"" + byNameAvatar + + "\",\"display_name\":\"" + byNameDisplay + "\",\"user_id\":\"" + byNameId + "\"}]}"; + private String byNameEmptyResponse = "{\"limited\":false,\"results\":[]}"; + + private String byThreepidSearch = "jane"; + private String byThreepidAvatar = "http://domain.tld/path/to/avatar.png"; + private String byThreepidDisplay = "John Doe"; + private String byThreepidId = "john.doe"; + private String byThreepidRequest = "{\"by\":\"threepid\",\"search_term\":\"" + byThreepidSearch + "\"}"; + private String byThreepidResponse = "{\"limited\":false,\"results\":[{\"avatar_url\":\"" + byThreepidAvatar + + "\",\"display_name\":\"" + byThreepidDisplay + "\",\"user_id\":\"" + byThreepidId + "\"}]}"; + private String byThreepidEmptyResponse = "{\"limited\":false,\"results\":[]}"; + + @Before + public void before() { + MatrixConfig mxCfg = new MatrixConfig(); + mxCfg.setDomain(domain); + mxCfg.build(); + + RestBackendConfig cfg = new RestBackendConfig(); + cfg.setEnabled(true); + cfg.setHost("http://localhost:65000"); + cfg.getEndpoints().setDirectory(endpoint); + cfg.build(); + + p = new RestDirectoryProvider(cfg, mxCfg); + } + + @Test + public void byNameFound() { + stubFor(post(urlEqualTo(endpoint)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(byNameResponse) + ) + ); + + UserDirectorySearchResult result = p.searchByDisplayName(byNameSearch); + assertTrue(!result.isLimited()); + assertTrue(result.getResults().size() == 1); + UserDirectorySearchResult.Result entry = result.getResults().get(0); + assertNotNull(entry); + assertTrue(StringUtils.equals(byNameAvatar, entry.getAvatarUrl())); + assertTrue(StringUtils.equals(byNameDisplay, entry.getDisplayName())); + assertTrue(StringUtils.equals(new MatrixID(byNameId, domain).getId(), entry.getUserId())); + + verify(postRequestedFor(urlMatching(endpoint)) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(equalTo(byNameRequest)) + ); + } + + @Test + public void byNameNotFound() { + stubFor(post(urlEqualTo(endpoint)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(byNameEmptyResponse) + ) + ); + + UserDirectorySearchResult result = p.searchByDisplayName(byNameSearch); + assertTrue(!result.isLimited()); + assertTrue(result.getResults().isEmpty()); + + verify(postRequestedFor(urlMatching(endpoint)) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(equalTo(byNameRequest)) + ); + } + + @Test + public void byThreepidFound() { + stubFor(post(urlEqualTo(endpoint)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(new String(byThreepidResponse.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8)) + ) + ); + + UserDirectorySearchResult result = p.searchBy3pid(byThreepidSearch); + assertTrue(!result.isLimited()); + assertTrue(result.getResults().size() == 1); + UserDirectorySearchResult.Result entry = result.getResults().get(0); + assertNotNull(entry); + assertTrue(StringUtils.equals(byThreepidAvatar, entry.getAvatarUrl())); + assertTrue(StringUtils.equals(byThreepidDisplay, entry.getDisplayName())); + assertTrue(StringUtils.equals(new MatrixID(byThreepidId, domain).getId(), entry.getUserId())); + + verify(postRequestedFor(urlMatching(endpoint)) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(equalTo(byThreepidRequest)) + ); + } + + @Test + public void byThreepidNotFound() { + stubFor(post(urlEqualTo(endpoint)) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(byThreepidEmptyResponse) + ) + ); + + UserDirectorySearchResult result = p.searchBy3pid(byThreepidSearch); + assertTrue(!result.isLimited()); + assertTrue(result.getResults().isEmpty()); + + verify(postRequestedFor(urlMatching(endpoint)) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(equalTo(byThreepidRequest)) + ); + } + +} diff --git a/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java b/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java index 0289b12..97dda60 100644 --- a/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java +++ b/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java @@ -53,7 +53,7 @@ public class RestThreePidProviderTest { cfg.setEnabled(true); cfg.setHost("http://localhost:65000"); cfg.getEndpoints().getIdentity().setSingle(lookupSinglePath); - cfg.getEndpoints().getIdentity().setBulk("/lookup/bulk"); + cfg.getEndpoints().getIdentity().setBulk(lookupBulkPath); cfg.build(); p = new RestThreePidProvider(cfg, mxCfg);