Skip to content

Commit

Permalink
✨ handle application approved/declined
Browse files Browse the repository at this point in the history
  • Loading branch information
ebullient committed Jun 11, 2024
1 parent 12ea334 commit ec4a13e
Show file tree
Hide file tree
Showing 10 changed files with 756 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ public static String getLogin(DataCommonItem issue) {

public static boolean isMemberApplicationEvent(DataCommonItem issue, DataLabel label) {
return issue.title.startsWith("Membership application:")
&& !issue.closed
&& (ACCEPTED.equals(label.name) || DECLINED.equals(label.name));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,16 @@ public class MemberApplicationProcess {
@Inject
CommonhausDatastore datastore;

public void handleApplicationEvent(ScopedQueryContext qc, GHIssue issue, DataCommonItem item, DataLabel label) {
if (!ApplicationData.isMemberApplicationEvent(item, label)) {
return;
}

/**
* Handle an application event (issue label change) for a membership application.
* Not triggered by the user, but by the system when a label is added or removed from an issue.
*
* @param qc QueryContext for the repository containing the issue
* @param issue The issue that was updated
* @param item The issue as Json-derived data
* @param label The label that was added
*/
public void handleApplicationLabelAdded(ScopedQueryContext qc, GHIssue issue, DataCommonItem item, DataLabel label) {
String login = ApplicationData.getLogin(item);
GHUser applicant = qc.getUser(login);
CommonhausUser user = datastore.getCommonhausUser(login, applicant.getId(), false, false);
Expand Down Expand Up @@ -75,6 +80,7 @@ public void handleApplicationEvent(ScopedQueryContext qc, GHIssue issue, DataCom
if (u.status().updateFromPending()) {
u.status(MemberStatus.DECLINED);
}
u.isMember = false;
},
"Membership application declined",
true,
Expand All @@ -86,6 +92,15 @@ public void handleApplicationEvent(ScopedQueryContext qc, GHIssue issue, DataCom
}
}

/**
* User action called when a user submits or updates their membership application
*
* @param session User session
* @param qc Datastore QueryContext (for making changes to the application issue)
* @param applicationData Current application data
* @param applicationPost Application data from the user
* @return updated ApplicationData object or null on error (see QueryContext for errors)
*/
public ApplicationData userUpdateApplicationIssue(
MemberSession session,
ScopedQueryContext qc,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.json.JsonObject;

import org.commonhaus.automation.admin.AdminDataCache;
import org.commonhaus.automation.admin.api.ApplicationData;
import org.commonhaus.automation.admin.api.MemberApplicationProcess;
import org.commonhaus.automation.admin.config.UserManagementConfig.AttestationConfig;
import org.commonhaus.automation.github.context.ActionType;
Expand Down Expand Up @@ -86,13 +87,18 @@ public void updateMembership(GitHub github, DynamicGraphQLClient graphQLClient,
* @param graphQLClient
* @param issueCommentEvent
*/
public void updateApplication(GitHubEvent event, GitHub github, DynamicGraphQLClient graphQLClient,
public void applicationIssueLabelAdded(GitHubEvent event, GitHub github, DynamicGraphQLClient graphQLClient,
@Issue.Labeled GHEventPayload.Issue issueEvent) {

String repoFullName = issueEvent.getRepository().getFullName();
ActionType actionType = ActionType.fromString(event.getAction());
JsonObject payload = JsonAttribute.unpack(event.getPayload());
DataCommonItem issue = JsonAttribute.issue.commonItemFrom(payload);
DataLabel label = JsonAttribute.label.labelFrom(payload);

// ignore if it isn't an issue in the datastore repository
if (!repoFullName.equals(ctx.getDataStore())) {
if (!repoFullName.equals(ctx.getDataStore())
|| !ApplicationData.isMemberApplicationEvent(issue, label)) {
return;
}

Expand All @@ -102,15 +108,11 @@ public void updateApplication(GitHubEvent event, GitHub github, DynamicGraphQLCl
issueEvent.getInstallation().getId())
.addExisting(graphQLClient);

JsonObject payload = JsonAttribute.unpack(event.getPayload());
DataCommonItem issue = JsonAttribute.issue.commonItemFrom(payload);
DataLabel label = JsonAttribute.label.labelFrom(payload);

Log.debugf("[%s] updateApplication #%s - %s", qc.getLogId(),
issue.number, actionType);

try {
applicationProcess.handleApplicationEvent(qc, issueEvent.getIssue(), issue, label);
applicationProcess.handleApplicationLabelAdded(qc, issueEvent.getIssue(), issue, label);
} catch (Exception e) {
ctx.logAndSendEmail(qc.getLogId(), "Error with issue label event", e, null);
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package org.commonhaus.automation.admin.api;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.util.Set;
import java.util.stream.Stream;

import jakarta.inject.Inject;

import org.commonhaus.automation.admin.AdminDataCache;
import org.commonhaus.automation.admin.api.CommonhausUser.MemberStatus;
import org.commonhaus.automation.admin.config.AdminConfigFile;
import org.commonhaus.automation.admin.github.AppContextService;
import org.commonhaus.automation.admin.github.ContextHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.kohsuke.github.GHContentBuilder;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHTeam;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkiverse.githubapp.testing.GitHubAppTest;
import io.quarkiverse.githubapp.testing.GitHubAppTesting;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.test.junit.QuarkusTest;
import io.smallrye.graphql.client.Response;

@QuarkusTest
@GitHubAppTest
public class ApplicationEventTest extends ContextHelper {

@Inject
MockMailbox mailbox;

@Inject
AppContextService ctx;

@Inject
ObjectMapper mapper;

@BeforeEach
void init() throws IOException {
mailbox.clear();
Stream.of(AdminDataCache.values()).forEach(AdminDataCache::invalidateAll);
}

@Test
void testApplicationApproved() throws Exception {
// When a discussion is labeled, ...
// from src/test/resources/github/eventIssueLabeled-accepted.json
String issueId = "I_kwDOL8tG0s6Lx52p";

// preload the cache: no request to fetch labels (and check our work)
setLabels(issueId, Set.of());

Response removeLabel = mockResponse("src/test/resources/github/mutableRemoveLabelsFromLabelable.json");
final GHContentBuilder builder = Mockito.mock(GHContentBuilder.class);

GitHubAppTesting.given()
.github(mocks -> {
mocks.configFile(AdminConfigFile.NAME).fromClasspath("/cf-admin.yml");

GitHub botGithub = setupBotGithub(ctx, mocks);
when(botGithub.isCredentialValid()).thenReturn(true);

setupMockTeam(mocks);
mockExistingCommonhausData(botGithub, ctx, "src/test/resources/haus-member-application.yaml");

mockUpdateCommonhausData(builder, botGithub, ctx);

when(mocks.installationGraphQLClient(installationId)
.executeSync(contains("removeLabelsFromLabelable("), anyMap()))
.thenReturn(removeLabel);
})
.when().payloadFromClasspath("/github/eventIssueLabeled-accepted.json")
.event(GHEvent.ISSUES)
.then().github(mocks -> {

// 1) Set member flag, move status from UNKNOWN -> ACTIVE
// 2) remove application
final ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
verify(builder).content(contentCaptor.capture());
var result = AppContextService.yamlMapper().readValue(contentCaptor.getValue(), CommonhausUser.class);

assertThat(result.application).isNull();
assertThat(result.isMember).isTrue();
assertThat(result.data.status).isEqualTo(MemberStatus.ACTIVE); // changed from UNKNOWN -> PENDING

// 3) add user to target team
GHTeam target = mocks.team("team-quorum-default".hashCode());
verify(target).add(any(GHUser.class));

// 4) Close issue
verify(mocks.issue(2345115049L)).close();

// 5) remove application/new
verify(mocks.installationGraphQLClient(installationId), timeout(500))
.executeSync(contains("removeLabelsFromLabelable("), anyMap());

verifyNoMoreInteractions(mocks.installationGraphQLClient(installationId));
});

}

@Test
void testApplicationDenied() throws Exception {
// When a discussion is labeled, ...
// from src/test/resources/github/eventIssueLabeled-accepted.json
String issueId = "I_kwDOL8tG0s6Lx52p";

// preload the cache: no request to fetch labels (and check our work)
setLabels(issueId, Set.of());

Response removeLabel = mockResponse("src/test/resources/github/mutableRemoveLabelsFromLabelable.json");
final GHContentBuilder builder = Mockito.mock(GHContentBuilder.class);

GitHubAppTesting.given()
.github(mocks -> {
mocks.configFile(AdminConfigFile.NAME).fromClasspath("/cf-admin.yml");

GitHub botGithub = setupBotGithub(ctx, mocks);
when(botGithub.isCredentialValid()).thenReturn(true);

setupMockTeam(mocks);
mockExistingCommonhausData(botGithub, ctx, "src/test/resources/haus-member-application.yaml");

mockUpdateCommonhausData(builder, botGithub, ctx);

when(mocks.installationGraphQLClient(installationId)
.executeSync(contains("removeLabelsFromLabelable("), anyMap()))
.thenReturn(removeLabel);
})
.when().payloadFromClasspath("/github/eventIssueLabeled-declined.json")
.event(GHEvent.ISSUES)
.then().github(mocks -> {

// 1) Set member flag, move status from UNKNOWN -> DECLINED
final ArgumentCaptor<String> contentCaptor = ArgumentCaptor.forClass(String.class);
verify(builder).content(contentCaptor.capture());
var result = AppContextService.yamlMapper().readValue(contentCaptor.getValue(), CommonhausUser.class);

assertThat(result.application).isNotNull();
assertThat(result.isMember).isFalse();
assertThat(result.data.status).isEqualTo(MemberStatus.DECLINED); // changed from UNKNOWN -> PENDING

// 3) add user to target team
GHTeam target = mocks.team("team-quorum-default".hashCode());
verify(target, times(0)).add(any(GHUser.class));

// 4) Close issue
verify(mocks.issue(2345115049L)).close();

// 5) remove application/new
verify(mocks.installationGraphQLClient(installationId), timeout(500))
.executeSync(contains("removeLabelsFromLabelable("), anyMap());

verifyNoMoreInteractions(mocks.installationGraphQLClient(installationId));
});

}

}
Loading

0 comments on commit ec4a13e

Please sign in to comment.