Commit cd2bc688 authored by Martin Lowe's avatar Martin Lowe 🇨🇦
Browse files

Add unit tests, add accounts lookup functionality

parent d1b52792
quarkus.datasource.username = sample
quarkus.datasource.password = sample
quarkus.datasource.jdbc.url=jdbc:mariadb://mariadb/eclipse
## Accounts.eclipse.org eclipsefdn_view_all_profiles scope access client
quarkus.oidc-client.client-id=sample
quarkus.oidc-client.credentials.secret=sample
\ No newline at end of file
......@@ -25,7 +25,6 @@ services:
memory: 192M
depends_on:
- mariadb
- keycloak
mariadb:
image: mariadb:latest
command: --max_allowed_packet=100000000
......@@ -37,36 +36,6 @@ services:
volumes:
- ./config/mariadb/ddl.sql:/docker-entrypoint-initdb.d/ddl.sql
- ./volumes/mariadb:/var/lib/mysql
postgres:
container_name: postgres
image: postgres:12.4
volumes:
- ./volumes/postgres:/var/lib/postgresql/data
environment:
- POSTGRES_DB=${REM_POSTGRES_DB}
- POSTGRES_USER=${REM_POSTGRES_USER}
- POSTGRES_PASSWORD=${REM_POSTGRES_PASSWORD}
ports:
- 5432
keycloak:
container_name: keycloak
image: jboss/keycloak:11.0.1
environment:
- VIRTUAL_HOST=keycloak
- VIRTUAL_PORT=8080
- DB_VENDOR=POSTGRES
- DB_DATABASE=${REM_POSTGRES_DB}
- DB_SCHEMA=public
- DB_ADDR=postgres
- DB_PORT=5432
- DB_USER=${REM_POSTGRES_USER}
- DB_PASSWORD=${REM_POSTGRES_PASSWORD}
- KEYCLOAK_USER=${REM_KEYCLOAK_USER}
- KEYCLOAK_PASSWORD=${REM_KEYCLOAK_PASSWORD}
ports:
- '8080:8080'
depends_on:
- postgres
nginx-proxy:
image: jwilder/nginx-proxy
ports:
......
......@@ -74,6 +74,18 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
......
......@@ -122,6 +122,11 @@ components:
type: openIdConnect
openIdConnectUrl: https://auth.eclipse.org/auth/realms/foundation/.well-known/openid-configuration
schemas:
NullableString:
description: A nullable String type value
type:
- 'null'
- string
DateTime:
type: string
format: datetime
......@@ -140,13 +145,13 @@ components:
type: string
description: placeholder
list_description:
type: string
$ref: '#/components/schemas/NullableString'
description: placeholder
project_id:
type: integer
type: string
description: placeholder
list_short_description:
type: string
$ref: '#/components/schemas/NullableString'
description: placeholder
create_archives:
type: boolean
......@@ -169,10 +174,10 @@ components:
create_date:
$ref: '#/components/schemas/DateTime'
created_by:
type: string
$ref: '#/components/schemas/NullableString'
description: placeholder
provision_status:
type: string
$ref: '#/components/schemas/NullableString'
description: placeholder
count:
type: integer
......
/*******************************************************************************
* Copyright (C) 2020 Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
******************************************************************************/
package org.eclipsefoundation.mailing.api;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.auto.value.AutoValue;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.quarkus.security.Authenticated;
/**
* Binding interface for the Eclipse Foundation user account API. Runtime implementations are automatically generated by
* Quarkus at compile time. As the API deals with sensitive information, authentication is required to access this
* endpoint.
*
* @author Martin Lowe
*
*/
@OidcClientFilter
@Authenticated
@Path("/account")
@RegisterRestClient(configKey = "accounts")
public interface AccountsAPI {
/**
* Retrieves all user objects that match the given query parameters.
*
* @param id user ID of the Eclipse account to retrieve
* @return all matching eclipse accounts
*/
@GET
@Path("/profile/{uid}")
@Produces("application/json")
EclipseUser getUsers(@PathParam("uid") String id);
@AutoValue
@JsonDeserialize(builder = AutoValue_AccountsAPI_EclipseUser.Builder.class)
public abstract static class EclipseUser {
public abstract int getUid();
public abstract String getName();
public abstract String getMail();
public abstract ECA getECA();
public abstract boolean getIsCommitter();
public static Builder builder() {
return new AutoValue_AccountsAPI_EclipseUser.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setUid(int id);
public abstract Builder setName(String name);
public abstract Builder setMail(String mail);
public abstract Builder setECA(ECA eca);
public abstract Builder setIsCommitter(boolean isCommitter);
public abstract EclipseUser build();
}
}
@AutoValue
@JsonDeserialize(builder = AutoValue_AccountsAPI_ECA.Builder.class)
public abstract static class ECA {
public abstract boolean getSigned();
public abstract boolean getCanContributeSpecProject();
public static Builder builder() {
return new AutoValue_AccountsAPI_ECA.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setSigned(boolean signed);
public abstract Builder setCanContributeSpecProject(boolean canContributeSpecProject);
public abstract ECA build();
}
}
}
......@@ -12,6 +12,7 @@ import javax.persistence.Table;
import javax.persistence.Transient;
import javax.ws.rs.core.MultivaluedMap;
import org.eclipsefoundation.mailing.namespace.MailingListUrlParamterNames;
import org.eclipsefoundation.persistence.dto.BareNode;
import org.eclipsefoundation.persistence.dto.filter.DtoFilter;
import org.eclipsefoundation.persistence.model.DtoTable;
......@@ -21,6 +22,7 @@ import org.eclipsefoundation.persistence.model.ParameterizedSQLStatementBuilder;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
@Table
@Entity
......@@ -142,6 +144,7 @@ public class MailingLists extends BareNode {
/**
* @return the isForNewsArchives
*/
@JsonProperty(value = "is_for_news_archives")
public boolean isForNewsArchives() {
return isForNewsArchives;
}
......@@ -156,6 +159,7 @@ public class MailingLists extends BareNode {
/**
* @return the isDisabled
*/
@JsonProperty(value = "is_disabled")
public boolean isDisabled() {
return isDisabled;
}
......@@ -170,6 +174,7 @@ public class MailingLists extends BareNode {
/**
* @return the isDeleted
*/
@JsonProperty(value = "is_deleted")
public boolean isDeleted() {
return isDeleted;
}
......@@ -184,6 +189,7 @@ public class MailingLists extends BareNode {
/**
* @return the isPrivate
*/
@JsonProperty(value = "is_private")
public boolean isPrivate() {
return isPrivate;
}
......@@ -198,6 +204,7 @@ public class MailingLists extends BareNode {
/**
* @return the isSubscribable
*/
@JsonProperty(value = "is_subscribable")
public boolean isSubscribable() {
return isSubscribable;
}
......@@ -303,34 +310,48 @@ public class MailingLists extends BareNode {
ParameterizedSQLStatement stmt = builder.build(TABLE);
if (isRoot) {
// ID check
String id = params.getFirst("list_name");
String id = params.getFirst(MailingListUrlParamterNames.LIST_NAME.getName());
if (id != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".listName = ?",
new Object[] { id }));
}
// project ID check
String projectId = params.getFirst("project_id");
String projectId = params.getFirst(MailingListUrlParamterNames.PROJECT_ID.getName());
if (projectId != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".projectId = ?",
new Object[] { projectId }));
}
// subscribable check
boolean isSubscribable = Boolean.parseBoolean(params.getFirst("is_subscribable"));
if (isSubscribable) {
String isSubscribable = params.getFirst(MailingListUrlParamterNames.IS_SUBSCRIBABLE.getName());
if (isSubscribable != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".isSubscribable = ?",
new Object[] { isSubscribable }));
new Object[] { Boolean.parseBoolean(isSubscribable) }));
}
// is private check
boolean isPrivate = Boolean.parseBoolean(params.getFirst("is_private"));
if (isPrivate) {
String isPrivate = params.getFirst(MailingListUrlParamterNames.IS_PRIVATE.getName());
if (isPrivate != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".isPrivate = ?",
new Object[] { isPrivate }));
new Object[] { Boolean.parseBoolean(isPrivate) }));
}
// is deleted check
boolean isDeleted = Boolean.parseBoolean(params.getFirst("is_deleted"));
if (isDeleted) {
String isDeleted = params.getFirst(MailingListUrlParamterNames.IS_DELETED.getName());
if (isDeleted != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".isDeleted = ?",
new Object[] { isDeleted }));
new Object[] { Boolean.parseBoolean(isDeleted) }));
}
// is disabled check
String isDisabled = params.getFirst(MailingListUrlParamterNames.IS_DISABLED.getName());
if (isDisabled != null) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".isDisabled = ?",
new Object[] { Boolean.parseBoolean(isDisabled) }));
}
// subscriber check
String userEmail = params.getFirst(MailingListUrlParamterNames.USER_EMAIL.getName());
if (userEmail != null) {
stmt.addJoin(new ParameterizedSQLStatement.Join(TABLE, MailingListSubscriptions.TABLE, "listName",
"listName"));
stmt.addClause(new ParameterizedSQLStatement.Clause(
MailingListSubscriptions.TABLE.getAlias() + ".email = ?", new Object[] { userEmail }));
}
}
return stmt;
......
package org.eclipsefoundation.mailing.namespace;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.inject.Singleton;
import org.eclipsefoundation.core.namespace.UrlParameterNamespace;
@Singleton
public final class MailingListUrlParamterNames implements UrlParameterNamespace {
public static final UrlParameter LIST_NAME = new UrlParameter("list_name");
public static final UrlParameter PROJECT_ID = new UrlParameter("project_id");
public static final UrlParameter IS_PRIVATE = new UrlParameter("is_private");
public static final UrlParameter IS_SUBSCRIBABLE = new UrlParameter("is_subscribable");
public static final UrlParameter IS_DISABLED = new UrlParameter("is_disabled");
public static final UrlParameter IS_DELETED = new UrlParameter("is_deleted");
public static final UrlParameter USERNAME = new UrlParameter("username");
public static final UrlParameter USER_EMAIL = new UrlParameter("user_email");
private static final List<UrlParameter> params = Collections.unmodifiableList(Arrays.asList(LIST_NAME, PROJECT_ID,
IS_PRIVATE, IS_SUBSCRIBABLE, IS_DISABLED, IS_DELETED, USERNAME, USER_EMAIL));
@Override
public List<UrlParameter> getParameters() {
return new ArrayList<>(params);
}
}
......@@ -9,22 +9,31 @@ import javax.ws.rs.GET;
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.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.core.model.RequestWrapper;
import org.eclipsefoundation.core.service.CachingService;
import org.eclipsefoundation.mailing.api.AccountsAPI;
import org.eclipsefoundation.mailing.api.AccountsAPI.EclipseUser;
import org.eclipsefoundation.mailing.dto.MailingListSubscriptions;
import org.eclipsefoundation.mailing.dto.MailingLists;
import org.eclipsefoundation.mailing.namespace.MailingListUrlParamterNames;
import org.eclipsefoundation.persistence.dao.impl.DefaultHibernateDao;
import org.eclipsefoundation.persistence.model.RDBMSQuery;
import org.eclipsefoundation.persistence.service.FilterService;
import org.jboss.resteasy.client.exception.ResteasyWebApplicationException;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Path("/mailing_lists")
@Path("/mailing-list")
@Produces(MediaType.APPLICATION_JSON)
public class MailingListsResource {
private static final Logger LOGGER = LoggerFactory.getLogger(MailingListsResource.class);
@Inject
DefaultHibernateDao dao;
......@@ -35,17 +44,33 @@ public class MailingListsResource {
@Inject
RequestWrapper wrap;
@Inject
@RestClient
AccountsAPI api;
@GET
public Response all() {
List<MailingLists> mailingLists = dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class)));
return Response.ok(mailingLists).build();
public Response all(@QueryParam("username") String uid) {
MultivaluedMap<String, String> params = getDefaultParams();
// check for user to retrieve
if (uid != null) {
try {
EclipseUser user = api.getUsers(uid);
if (user != null && user.getMail() != null) {
params.add(MailingListUrlParamterNames.USER_EMAIL.getName(), user.getMail());
}
} catch (ResteasyWebApplicationException e) {
LOGGER.warn("Could not find valid user with username '{}', returning empty results", uid);
return Response.ok(Collections.emptyList()).build();
}
}
return Response.ok(dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), params))).build();
}
@GET
@Path("{listName}")
public Response mailingList(@PathParam("listName") String listName) {
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add("list_name", listName);
MultivaluedMap<String, String> params = getDefaultParams();
params.add(MailingListUrlParamterNames.LIST_NAME.getName(), listName);
List<MailingLists> mailingLists = dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), params));
if (mailingLists.isEmpty()) {
return Response.status(404).build();
......@@ -60,14 +85,15 @@ public class MailingListsResource {
@Path("available")
public Response available() {
List<MailingLists> mailingLists = dao
.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), getDefaultParams()));
.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), getAvailableListParams()));
return Response.ok(mailingLists).build();
}
@GET
@Path("projects")
public Response getByProjects(@PathParam("projectId") String projectId) {
List<MailingLists> mailingLists = dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class)));
List<MailingLists> mailingLists = dao
.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), getDefaultParams()));
if (mailingLists.isEmpty()) {
return Response.ok(Collections.emptyList()).build();
}
......@@ -76,9 +102,11 @@ public class MailingListsResource {
}
@GET
@Path("projects/{projectId:\\d*}")
@Path("projects/{projectId}")
public Response getForProjectByID(@PathParam("projectId") String projectId) {
List<MailingLists> mailingLists = dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class)));
MultivaluedMap<String, String> params = getDefaultParams();
params.add(MailingListUrlParamterNames.PROJECT_ID.getName(), projectId);
List<MailingLists> mailingLists = dao.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), params));
if (mailingLists.isEmpty()) {
return Response.ok(Collections.emptyList()).build();
}
......@@ -89,7 +117,7 @@ public class MailingListsResource {
@Path("projects/available")
public Response getByProjectsAvailable(@PathParam("projectId") String projectId) {
List<MailingLists> mailingLists = dao
.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), getDefaultParams()));
.get(new RDBMSQuery<>(wrap, filters.get(MailingLists.class), getAvailableListParams()));
if (mailingLists.isEmpty()) {
return Response.ok(Collections.emptyList()).build();
}
......@@ -99,14 +127,20 @@ public class MailingListsResource {
/**
* Gets default params for available mailing lists
*
* @return
*/
private MultivaluedMap<String, String> getDefaultParams() {
MultivaluedMap<String, String> params = new MultivaluedMapImpl<>();
params.add("is_private", Boolean.FALSE.toString());
params.add("is_deleted", Boolean.FALSE.toString());
params.add("is_disabled", Boolean.FALSE.toString());
params.add("is_subscribable", Boolean.TRUE.toString());
params.add(MailingListUrlParamterNames.IS_PRIVATE.getName(), Boolean.FALSE.toString());
return params;
}
private MultivaluedMap<String, String> getAvailableListParams() {
MultivaluedMap<String, String> params = getDefaultParams();
params.add(MailingListUrlParamterNames.IS_DELETED.getName(), Boolean.FALSE.toString());
params.add(MailingListUrlParamterNames.IS_DISABLED.getName(), Boolean.FALSE.toString());
params.add(MailingListUrlParamterNames.IS_SUBSCRIBABLE.getName(), Boolean.TRUE.toString());
return params;
}
}
\ No newline at end of file
......@@ -10,7 +10,13 @@ quarkus.datasource.db-kind=mariadb
quarkus.datasource.jdbc.min-size = 5
quarkus.datasource.jdbc.max-size = 15
accounts/mp-rest/url=https://api.eclipse.org
quarkus.oauth2.enabled=false
quarkus.oidc-client.auth-server-url=https://accounts.eclipse.org/oauth2
quarkus.oidc-client.discovery-enabled=false
quarkus.oidc-client.token-path=/token
quarkus.oidc-client.grant.type=client
quarkus.oidc-client.scopes=eclipsefdn_view_all_profiles
# Tells Quarkus which objects are associated with what databases (used to generate entity tables internally)
quarkus.hibernate-orm.packages=org.eclipsefoundation.mailing.dto
......
package org.eclipsefoundation.mailing.resources;
import static io.restassured.RestAssured.given;
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
import javax.inject.Inject;
import org.eclipsefoundation.mailing.test.namespace.SchemaNamespaceHelper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.test.junit.QuarkusTest;
@TestInstance(Lifecycle.PER_CLASS)
@QuarkusTest
class MailingListsResourceTests {
public static final String MAILING_LISTS_BASE_URL = "/mailing-list";
public static final String AVAILABLE_MAILING_LISTS_URL = MAILING_LISTS_BASE_URL + "/available";
public static final String MAILING_LISTS_BY_NAME_URL = MAILING_LISTS_BASE_URL + "/{listName}";
public static final String PROJECTS_MAILING_LISTS_URL = MAILING_LISTS_BASE_URL + "/projects";
public static final String AVAILABLE_PROJECTS_MAILING_LISTS_URL = PROJECTS_MAILING_LISTS_URL + "/available";
public static final String PROJECT_MAILING_LISTS_URL = PROJECTS_MAILING_LISTS_URL + "/{projectID}";
@Inject
ObjectMapper json;
@Test
void getMailingLists_success() {
given().when().get(MAILING_LISTS_BASE_URL).then().statusCode(200);
}
@Test
void getMailingLists_success_format() {
given().when().get(MAILING_LISTS_BASE_URL).then().assertThat()
.body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.MAILING_LISTS_SCHEMA_PATH));
}
@Test
void getAvailableMailingLists_success() {
given().when().get(AVAILABLE_MAILING_LISTS_URL).then().statusCode(200);
}
@Test
void getAvailableMailingLists_success_format() {
given().when().get(AVAILABLE_MAILING_LISTS_URL).then().assertThat()
.body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.MAILING_LISTS_SCHEMA_PATH));
}
@Test
void getMailingList_success() {
given().when().get(MAILING_LISTS_BY_NAME_URL, "eclipse-dev").then().statusCode(200);
}
@Test
void getMailingList_success_format() {
given().when().get(MAILING_LISTS_BY_NAME_URL, "eclipse-dev").then().assertThat()
.body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.MAILING_LIST_SCHEMA_PATH));
}
@Test
void getProjectsMailingLists_success() {
given().when().get(PROJECTS_MAILING_LISTS_URL).then().statusCode(200);
}
@Test
void getProjectsMailingLists_success_format() {
given().when().get(PROJECTS_MAILING_LISTS_URL).then().assertThat()
.body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.MAILING_LIST_MAPPING_SCHEMA_PATH));
}