| package com.googlesource.gerrit.plugins.chatgpt; |
| |
| import com.github.tomakehurst.wiremock.client.WireMock; |
| import com.github.tomakehurst.wiremock.junit.WireMockRule; |
| import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder; |
| import com.github.tomakehurst.wiremock.verification.LoggedRequest; |
| import com.google.common.net.HttpHeaders; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.BranchNameKey; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.config.PluginConfig; |
| import com.google.gerrit.server.data.AccountAttribute; |
| import com.google.gerrit.server.data.PatchSetAttribute; |
| import com.google.gerrit.server.events.CommentAddedEvent; |
| import com.google.gerrit.server.events.Event; |
| import com.google.gerrit.server.events.PatchSetCreatedEvent; |
| import com.google.gerrit.server.events.PatchSetEvent; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonObject; |
| import com.google.inject.AbstractModule; |
| import com.google.inject.Guice; |
| import com.googlesource.gerrit.plugins.chatgpt.config.ConfigCreator; |
| import com.googlesource.gerrit.plugins.chatgpt.config.Configuration; |
| import com.googlesource.gerrit.plugins.chatgpt.data.PluginDataHandler; |
| import com.googlesource.gerrit.plugins.chatgpt.listener.EventHandlerTask; |
| import com.googlesource.gerrit.plugins.chatgpt.listener.GerritEventContextModule; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.UriResourceLocator; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientFacade; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.gerrit.IGerritClientPatchSet; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.chatgpt.ChatGptClientStateful; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.gerrit.GerritClientPatchSetStateful; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.stateful.client.api.git.GitRepoFiles; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.chatgpt.ChatGptClientStateless; |
| import com.googlesource.gerrit.plugins.chatgpt.mode.stateless.client.api.gerrit.GerritClientPatchSetStateless; |
| import lombok.NonNull; |
| import org.apache.http.entity.ContentType; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.mockito.Mock; |
| import org.mockito.Mockito; |
| import org.mockito.stubbing.Answer; |
| |
| import java.io.IOException; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.time.Instant; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.function.Consumer; |
| |
| import static com.google.gerrit.extensions.client.ChangeKind.REWORK; |
| import static com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.UriResourceLocator.*; |
| import static com.googlesource.gerrit.plugins.chatgpt.utils.GsonUtils.getGson; |
| import static java.net.HttpURLConnection.HTTP_OK; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.when; |
| |
| public class ChatGptReviewTestBase { |
| protected static final Path basePath = Paths.get("src/test/resources"); |
| protected static final String GERRIT_AUTH_BASE_URL = "http://localhost:9527"; |
| protected static final int GERRIT_GPT_ACCOUNT_ID = 1000000; |
| protected static final String GERRIT_GPT_USERNAME = "gpt"; |
| protected static final int GERRIT_USER_ACCOUNT_ID = 1000001; |
| protected static final String GERRIT_USER_ACCOUNT_NAME = "Test"; |
| protected static final String GERRIT_USER_ACCOUNT_EMAIL = "test@example.com"; |
| protected static final String GERRIT_USER_USERNAME = "test"; |
| protected static final String GERRIT_USER_PASSWORD = "test"; |
| protected static final String GERRIT_USER_GROUP = "Test"; |
| protected static final String GPT_TOKEN = "tk-test"; |
| protected static final String GPT_DOMAIN = "http://localhost:9527"; |
| protected static final Project.NameKey PROJECT_NAME = Project.NameKey.parse("myProject"); |
| protected static final Change.Key CHANGE_ID = Change.Key.parse("myChangeId"); |
| protected static final BranchNameKey BRANCH_NAME = BranchNameKey.create(PROJECT_NAME, "myBranchName"); |
| protected static final boolean GPT_STREAM_OUTPUT = true; |
| protected static final long TEST_TIMESTAMP = 1699270812; |
| private static final int GPT_USER_ACCOUNT_ID = 1000000; |
| |
| @Rule |
| public WireMockRule wireMockRule = new WireMockRule(9527); |
| |
| @Mock |
| protected GitRepoFiles gitRepoFiles; |
| |
| @Mock |
| protected PluginDataHandler pluginDataHandler; |
| |
| protected PluginConfig globalConfig; |
| protected PluginConfig projectConfig; |
| protected Configuration config; |
| protected GerritClient gerritClient; |
| protected PatchSetReviewer patchSetReviewer; |
| protected ConfigCreator mockConfigCreator; |
| protected List<LoggedRequest> loggedRequests; |
| protected JsonArray prompts; |
| |
| @Before |
| public void before() throws NoSuchProjectException { |
| initGlobalAndProjectConfig(); |
| initConfig(); |
| setupMockRequests(); |
| initComparisonContent(); |
| initTest(); |
| } |
| |
| protected GerritChange getGerritChange() { |
| return new GerritChange(PROJECT_NAME, BRANCH_NAME, CHANGE_ID); |
| } |
| |
| protected void initGlobalAndProjectConfig() { |
| globalConfig = mock(PluginConfig.class); |
| Answer<Object> returnDefaultArgument = invocation -> { |
| // Return the second argument (i.e., the Default value) passed to the method |
| return invocation.getArgument(1); |
| }; |
| |
| // Mock the Global Config values not provided by Default |
| when(globalConfig.getString("gerritAuthBaseUrl")).thenReturn(GERRIT_AUTH_BASE_URL); |
| when(globalConfig.getString("gerritUserName")).thenReturn(GERRIT_USER_USERNAME); |
| when(globalConfig.getString("gerritPassword")).thenReturn(GERRIT_USER_PASSWORD); |
| when(globalConfig.getString("gptToken")).thenReturn(GPT_TOKEN); |
| |
| // Mock the Global Config values to the Defaults passed as second arguments of the `get*` methods. |
| when(globalConfig.getString(Mockito.anyString(), Mockito.anyString())).thenAnswer(returnDefaultArgument); |
| when(globalConfig.getInt(Mockito.anyString(), Mockito.anyInt())).thenAnswer(returnDefaultArgument); |
| when(globalConfig.getBoolean(Mockito.anyString(), Mockito.anyBoolean())).thenAnswer(returnDefaultArgument); |
| |
| // Mock the Global Config values that differ from the ones provided by Default |
| when(globalConfig.getString(Mockito.eq("gptDomain"), Mockito.anyString())) |
| .thenReturn(GPT_DOMAIN); |
| |
| projectConfig = mock(PluginConfig.class); |
| |
| // Mock the Project Config values |
| when(projectConfig.getBoolean(Mockito.eq("isEnabled"), Mockito.anyBoolean())).thenReturn(true); |
| } |
| |
| protected void initConfig() { |
| config = new Configuration(globalConfig, projectConfig); |
| |
| // Mock the config instance values |
| when(config.getGerritUserName()).thenReturn(GERRIT_GPT_USERNAME); |
| } |
| |
| protected void setupMockRequests() { |
| String fullChangeId = getGerritChange().getFullChangeId(); |
| |
| // Mock the behavior of the gerritAccountIdUri request |
| WireMock.stubFor(WireMock.get(gerritAccountIdUri(GERRIT_GPT_USERNAME)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBody("[{\"_account_id\": " + GERRIT_GPT_ACCOUNT_ID + "}]"))); |
| |
| // Mock the behavior of the gerritAccountIdUri request |
| WireMock.stubFor(WireMock.get(gerritAccountIdUri(GERRIT_USER_USERNAME)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBody("[{\"_account_id\": " + GERRIT_USER_ACCOUNT_ID + "}]"))); |
| |
| // Mock the behavior of the gerritAccountGroups request |
| WireMock.stubFor(WireMock.get(UriResourceLocator.gerritAccountsUri() + |
| gerritGroupPostfixUri(GERRIT_USER_ACCOUNT_ID)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBodyFile("gerritAccountGroups.json"))); |
| |
| // Mock the behavior of the gerritPatchSetRevisionsUri request |
| WireMock.stubFor(WireMock.get(gerritPatchSetRevisionsUri(fullChangeId)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBody("{\"revisions\":{\"aa5be5ebb80846475ec4dfe43e0799eb73c6415a\":{}}}"))); |
| |
| // Mock the behavior of the gerritGetPatchSetDetailUri request |
| WireMock.stubFor(WireMock.get(gerritGetPatchSetDetailUri(fullChangeId)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBodyFile("gerritPatchSetDetail.json"))); |
| |
| // Mock the behavior of the gerritPatchSet comments request |
| WireMock.stubFor(WireMock.get(gerritGetAllPatchSetCommentsUri(fullChangeId)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK) |
| .withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()) |
| .withBodyFile("gerritPatchSetComments.json"))); |
| |
| // Mock the behavior of the postReview request |
| WireMock.stubFor(WireMock.post(gerritSetReviewUri(fullChangeId)) |
| .willReturn(WireMock.aResponse() |
| .withStatus(HTTP_OK))); |
| } |
| |
| protected void initComparisonContent() {} |
| |
| protected String readTestFile(String filename) { |
| try { |
| return new String(Files.readAllBytes(basePath.resolve(filename))); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected EventHandlerTask.Result handleEventBasedOnType(boolean isCommentEvent) { |
| Consumer<Event> typeSpecificSetup = getTypeSpecificSetup(isCommentEvent); |
| Event event = isCommentEvent ? mock(CommentAddedEvent.class) : mock(PatchSetCreatedEvent.class); |
| setupCommonEventMocks((PatchSetEvent) event); // Apply common mock configurations |
| typeSpecificSetup.accept(event); |
| |
| EventHandlerTask task = Guice.createInjector(new AbstractModule() { |
| @Override |
| protected void configure() { |
| install(new GerritEventContextModule(config, event)); |
| |
| bind(GerritClient.class).toInstance(gerritClient); |
| bind(GitRepoFiles.class).toInstance(gitRepoFiles); |
| bind(ConfigCreator.class).toInstance(mockConfigCreator); |
| bind(PatchSetReviewer.class).toInstance(patchSetReviewer); |
| bind(PluginDataHandler.class).toInstance(pluginDataHandler); |
| bind(AccountCache.class).toInstance(mockAccountCache()); |
| } |
| }).getInstance(EventHandlerTask.class); |
| return task.execute(); |
| } |
| |
| protected void testRequestSent() { |
| RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor( |
| WireMock.urlEqualTo(gerritSetReviewUri(getGerritChange().getFullChangeId()))); |
| loggedRequests = WireMock.findAll(requestPatternBuilder); |
| Assert.assertEquals(1, loggedRequests.size()); |
| JsonObject gptRequestBody = getGson().fromJson(patchSetReviewer.getChatGptClient().getRequestBody(), |
| JsonObject.class); |
| prompts = gptRequestBody.get("messages").getAsJsonArray(); |
| } |
| |
| private void initTest() { |
| ChangeSetData changeSetData = new ChangeSetData(GPT_USER_ACCOUNT_ID, config.getVotingMinScore(), config.getMaxReviewFileSize()); |
| gerritClient = new GerritClient(new GerritClientFacade(config, changeSetData, getGerritClientPatchSet())); |
| patchSetReviewer = new PatchSetReviewer(gerritClient, config, changeSetData, getChatGptClient()); |
| mockConfigCreator = mock(ConfigCreator.class); |
| } |
| |
| private AccountAttribute createTestAccountAttribute() { |
| AccountAttribute accountAttribute = new AccountAttribute(); |
| accountAttribute.name = GERRIT_USER_ACCOUNT_NAME; |
| accountAttribute.username = GERRIT_USER_USERNAME; |
| accountAttribute.email = GERRIT_USER_ACCOUNT_EMAIL; |
| return accountAttribute; |
| } |
| |
| private PatchSetAttribute createPatchSetAttribute() { |
| PatchSetAttribute patchSetAttribute = new PatchSetAttribute(); |
| patchSetAttribute.kind = REWORK; |
| patchSetAttribute.author = createTestAccountAttribute(); |
| return patchSetAttribute; |
| } |
| |
| @NonNull |
| private Consumer<Event> getTypeSpecificSetup(boolean isCommentEvent) { |
| Consumer<Event> typeSpecificSetup; |
| |
| if (isCommentEvent) { |
| typeSpecificSetup = event -> { |
| CommentAddedEvent commentEvent = (CommentAddedEvent) event; |
| commentEvent.author = this::createTestAccountAttribute; |
| commentEvent.patchSet = this::createPatchSetAttribute; |
| commentEvent.eventCreatedOn = TEST_TIMESTAMP; |
| when(commentEvent.getType()).thenReturn("comment-added"); |
| }; |
| } else { |
| typeSpecificSetup = event -> { |
| PatchSetCreatedEvent patchEvent = (PatchSetCreatedEvent) event; |
| patchEvent.patchSet = this::createPatchSetAttribute; |
| when(patchEvent.getType()).thenReturn("patchset-created"); |
| }; |
| } |
| return typeSpecificSetup; |
| } |
| |
| private void setupCommonEventMocks(PatchSetEvent event) { |
| when(event.getProjectNameKey()).thenReturn(PROJECT_NAME); |
| when(event.getBranchNameKey()).thenReturn(BRANCH_NAME); |
| when(event.getChangeKey()).thenReturn(CHANGE_ID); |
| } |
| |
| private AccountCache mockAccountCache() { |
| AccountCache accountCache = mock(AccountCache.class); |
| Account account = Account.builder(Account.id(GPT_USER_ACCOUNT_ID), Instant.now()).build(); |
| AccountState accountState = AccountState.forAccount(account, Collections.emptyList()); |
| doReturn(Optional.of(accountState)).when(accountCache).getByUsername(GERRIT_GPT_USERNAME); |
| |
| return accountCache; |
| } |
| |
| private IChatGptClient getChatGptClient() { |
| return switch (config.getGptMode()) { |
| case stateful -> new ChatGptClientStateful(); |
| case stateless -> new ChatGptClientStateless(); |
| }; |
| } |
| |
| private IGerritClientPatchSet getGerritClientPatchSet() { |
| return switch (config.getGptMode()) { |
| case stateful -> new GerritClientPatchSetStateful(config); |
| case stateless -> new GerritClientPatchSetStateless(config); |
| }; |
| } |
| } |