Skip to content

Commit

Permalink
Implement medium tests for new web socket events
Browse files Browse the repository at this point in the history
  • Loading branch information
nquinquenel committed Oct 30, 2023
1 parent 9e1db28 commit b528f3e
Show file tree
Hide file tree
Showing 13 changed files with 1,151 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
import com.google.gson.JsonObject;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonarsource.sonarlint.core.commons.log.ClientLogOutput;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
Expand Down Expand Up @@ -58,16 +60,17 @@ public static String getUrl() {
"SecurityHotspotChanged", new SecurityHotspotChangedEventParser(),
"TaintVulnerabilityClosed", new TaintVulnerabilityClosedEventParser(),
"TaintVulnerabilityRaised", new TaintVulnerabilityRaisedEventParser());

private static final Map<String, EventParser<?>> parsersByTypeForProjectUserFilter = Map.of(
"MyNewIssues", new SmartNotificationEventParser("NEW_ISSUES"));

private static final Map<String, EventParser<?>> parsersByType = Stream.of(parsersByTypeForProjectFilter, parsersByTypeForProjectUserFilter)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
private static final String PROJECT_FILTER_TYPE = "PROJECT";
private static final String PROJECT_USER_FILTER_TYPE = "PROJECT_USER";
private static final Gson gson = new Gson();
private WebSocket ws;
private final History history = new History();
private final ScheduledExecutorService sonarCloudWebSocketScheduler = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "sonarcloud-websocket-scheduled-jobs"));
private WebSocket ws;

public static SonarCloudWebSocket create(WebSocketClient webSocketClient, Consumer<ServerEvent> serverEventConsumer, Runnable connectionEndedRunnable) {
var webSocket = new SonarCloudWebSocket();
Expand Down Expand Up @@ -100,7 +103,9 @@ public void unsubscribe(String projectKey) {
}

private void send(String messageType, String projectKey, Map<String, EventParser<?>> parsersByType, String filter) {
var payload = new WebSocketEventSubscribePayload(messageType, parsersByType.keySet().toArray(new String[0]), filter, projectKey);
var eventsKey = parsersByType.keySet().toArray(new String[0]);
Arrays.sort(eventsKey);
var payload = new WebSocketEventSubscribePayload(messageType, eventsKey, filter, projectKey);

var jsonString = gson.toJson(payload);
SonarLintLogger.get().debug(String.format("sent '%s' for project '%s' and filter '%s'", messageType, projectKey, filter));
Expand Down Expand Up @@ -128,16 +133,12 @@ private static Optional<? extends ServerEvent> parse(WebSocketEvent event) {
return Optional.empty();
}

return Stream.of(parsersByTypeForProjectFilter, parsersByTypeForProjectUserFilter)
.flatMap(map -> map.entrySet().stream())
.filter(entry -> eventType.equals(entry.getKey()))
.map(Map.Entry::getValue)
.findFirst()
.map(parser -> tryParsing(parser, event))
.orElseGet(() -> {
SonarLintLogger.get().error("Unknown '{}' event type ", eventType);
return Optional.empty();
});
if (parsersByType.containsKey(eventType)) {
return tryParsing(parsersByType.get(eventType), event);
} else {
SonarLintLogger.get().error("Unknown '{}' event type ", eventType);
return Optional.empty();
}
}

private static Optional<? extends ServerEvent> tryParsing(EventParser<? extends ServerEvent> eventParser, WebSocketEvent event) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public WebSocketService(SonarLintClient client, ConnectionConfigurationRepositor
this.connectionConfigurationRepository = connectionConfigurationRepository;
this.configurationRepository = configurationRepository;
this.connectionAwareHttpClientProvider = connectionAwareHttpClientProvider;
this.shouldEnableWebSockets = params.getFeatureFlags().shouldManageSmartNotifications();
this.shouldEnableWebSockets = params.getFeatureFlags().shouldManageServerSentEvents();
this.storageFacade = storageService.getStorageFacade();
this.eventRouterByConnectionId = new HashMap<>();
this.client = client;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* SonarLint Core - Implementation
* Copyright (C) 2016-2023 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.websocket.parsing;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class SmartNotificationEventParserTests {

private SmartNotificationEventParser smartNotificationEventParser;

@Test
void should_parse_valid_json_date() {
smartNotificationEventParser = new SmartNotificationEventParser("QA");
var jsonData = "{\"message\": \"msg\", \"link\": \"lnk\", \"project\": \"projectKey\", \"date\": \"2023-07-19T15:08:01+0000\"}";

var optionalEvent = smartNotificationEventParser.parse(jsonData);

assertThat(optionalEvent).isPresent();
var event = optionalEvent.get();
assertThat(event.getCategory()).isEqualTo("QA");
assertThat(event.getDate()).isEqualTo("2023-07-19T15:08:01+0000");
assertThat(event.getMessage()).isEqualTo("msg");
assertThat(event.getProject()).isEqualTo("projectKey");
assertThat(event.getLink()).isEqualTo("lnk");
}

@Test
void should_not_parse_invalid_json_date() {
smartNotificationEventParser = new SmartNotificationEventParser("QA");
var jsonData = "{\"invalid\": \"msg\", \"link\": \"lnk\", \"project\": \"projectKey\", \"date\": \"2023-07-19T15:08:01+0000\"}";

var optionalEvent = smartNotificationEventParser.parse(jsonData);

assertThat(optionalEvent).isEmpty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/
package mediumtest;

import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
Expand All @@ -39,6 +40,7 @@
import org.sonarsource.sonarlint.core.clientapi.client.smartnotification.ShowSmartNotificationParams;
import org.sonarsource.sonarlint.core.serverapi.UrlUtils;
import testutils.MockWebServerExtensionWithProtobuf;
import testutils.websockets.WebSocketServer;

import static mediumtest.fixtures.SonarLintBackendFixture.newBackend;
import static mediumtest.fixtures.SonarLintBackendFixture.newFakeClient;
Expand All @@ -62,13 +64,6 @@ class SmartNotificationsMediumTests {
"\"project\": \"" + PROJECT_KEY + "\"," +
"\"date\": \"2022-01-01T08:00:00+0000\"," +
"\"category\": \"category\"}]}";

private static final String QG_EVENT_PROJECT_1 = "{\"events\": [" +
"{\"message\": \"msg1\"," +
"\"link\": \"lnk\"," +
"\"project\": \"" + PROJECT_KEY + "\"," +
"\"date\": \"2022-01-01T08:00:00+0000\"," +
"\"category\": \"QUALITY_GATE\"}]}";
private static final String EVENT_PROJECT_2 = "{\"events\": [" +
"{\"message\": \"msg2\"," +
"\"link\": \"lnk\"," +
Expand All @@ -87,9 +82,19 @@ class SmartNotificationsMediumTests {
"\"project\": \"" + PROJECT_KEY_3 + "\"," +
"\"date\": \"2022-01-01T08:00:00+0000\"," +
"\"category\": \"category\"}]}";
private static final String NEW_ISSUES_EVENT = "{\n" +
" \"event\": \"MyNewIssues\", \n" +
" \"data\": {\n" +
" \"message\": \"You have 3 new issues on project u0027SonarLint Coreu0027 on pull request u0027657u0027\",\n" +
" \"link\": \"link\",\n" +
" \"project\": \"" + PROJECT_KEY + "\",\n" +
" \"date\": \"2023-07-19T15:08:01+0000\"\n" +
" }\n" +
"}";
@RegisterExtension
private final MockWebServerExtensionWithProtobuf mockWebServerExtension = new MockWebServerExtensionWithProtobuf();
private SonarLintBackendImpl backend;
private WebSocketServer webSocketServer;

@BeforeEach
void prepare() {
Expand All @@ -104,6 +109,9 @@ void tearDown() throws ExecutionException, InterruptedException {
} else {
System.setProperty("sonarlint.internal.sonarcloud.url", oldSonarCloudUrl);
}
if (webSocketServer != null) {
webSocketServer.stop();
}
}

@Test
Expand Down Expand Up @@ -217,12 +225,12 @@ void it_should_send_notification_after_adding_removing_binding() {
}

@Test
void it_should_not_send_notification_handled_by_sonarcloud_websocket() {
void it_should_send_notification_handled_by_sonarcloud_websocket_as_fallback() {
var fakeClient = newFakeClient().build();
System.setProperty("sonarlint.internal.sonarcloud.url", mockWebServerExtension.endpointParams().getBaseUrl());
mockWebServerExtension.addResponse("/api/developers/search_events?projects=&from=", new MockResponse().setResponseCode(200));
mockWebServerExtension.addStringResponse("/api/developers/search_events?projects=" + PROJECT_KEY + "&from=" +
UrlUtils.urlEncode(STORED_DATE.format(TIME_FORMATTER)), QG_EVENT_PROJECT_1);
UrlUtils.urlEncode(STORED_DATE.format(TIME_FORMATTER)), EVENT_PROJECT_1);

backend = newBackend()
.withSonarCloudConnectionAndNotifications(CONNECTION_ID, "myOrg", storage ->
Expand All @@ -231,15 +239,19 @@ void it_should_not_send_notification_handled_by_sonarcloud_websocket() {
.withSmartNotifications()
.build(fakeClient);

await().atMost(3, TimeUnit.SECONDS).until(() -> fakeClient.getSmartNotificationsToShow().isEmpty());
await().atMost(3, TimeUnit.SECONDS).until(() -> !fakeClient.getSmartNotificationsToShow().isEmpty());

var notificationsResult = fakeClient.getSmartNotificationsToShow();
assertThat(notificationsResult).isEmpty();
assertThat(notificationsResult).hasSize(1);
assertThat(notificationsResult.get(0).getScopeIds()).hasSize(1).contains("scopeId");
}

@Test
void it_should_send_notification_not_yet_handled_by_sonarcloud_websocket() {
var fakeClient = newFakeClient().build();
void it_should_skip_polling_notifications_when_sonarcloud_websocket_opened() {
webSocketServer = new WebSocketServer();
webSocketServer.start();
System.setProperty("sonarlint.internal.sonarcloud.websocket.url", webSocketServer.getUrl());
var fakeClient = newFakeClient().withToken(CONNECTION_ID, "token").build();
System.setProperty("sonarlint.internal.sonarcloud.url", mockWebServerExtension.endpointParams().getBaseUrl());
mockWebServerExtension.addResponse("/api/developers/search_events?projects=&from=", new MockResponse().setResponseCode(200));
mockWebServerExtension.addStringResponse("/api/developers/search_events?projects=" + PROJECT_KEY + "&from=" +
Expand All @@ -250,11 +262,19 @@ void it_should_send_notification_not_yet_handled_by_sonarcloud_websocket() {
storage.withProject(PROJECT_KEY, project -> project.withLastSmartNotificationPoll(STORED_DATE)))
.withBoundConfigScope("scopeId", CONNECTION_ID, PROJECT_KEY)
.withSmartNotifications()
.withServerSentEventsEnabled()
.build(fakeClient);

await().atMost(3, TimeUnit.SECONDS).until(() -> !fakeClient.getSmartNotificationsToShow().isEmpty());
await().atMost(Duration.ofSeconds(2)).until(() -> webSocketServer.getConnections().size() == 1);

var notificationsResult = fakeClient.getSmartNotificationsToShow();
assertThat(notificationsResult).isEmpty();

webSocketServer.getConnections().get(0).sendMessage(NEW_ISSUES_EVENT);

await().atMost(2, TimeUnit.SECONDS).until(() -> !fakeClient.getSmartNotificationsToShow().isEmpty());

notificationsResult = fakeClient.getSmartNotificationsToShow();
assertThat(notificationsResult).hasSize(1);
assertThat(notificationsResult.get(0).getScopeIds()).hasSize(1).contains("scopeId");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ public SonarLintBackendBuilder withSecurityHotspotsEnabled() {
return this;
}

/**
* Also used to enable Web Sockets
*/
public SonarLintBackendBuilder withServerSentEventsEnabled() {
this.manageServerSentEvents = true;
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.sonarsource.sonarlint.core.clientapi.SonarLintClient;
import org.sonarsource.sonarlint.core.clientapi.backend.initialize.InitializeParams;
import org.sonarsource.sonarlint.core.local.only.LocalOnlyIssueStorageService;
import org.sonarsource.sonarlint.core.storage.StorageService;
import org.sonarsource.sonarlint.core.telemetry.TelemetryPathManager;

import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -69,6 +70,10 @@ public LocalOnlyIssueStorageService getLocalOnlyIssueStorageService() {
return getInitializedApplicationContext().getBean(LocalOnlyIssueStorageService.class);
}

public StorageService getIssueStorageService() {
return getInitializedApplicationContext().getBean(StorageService.class);
}

@Override
public CompletableFuture<Void> shutdown() {
return super.shutdown().whenComplete((v, t) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import jetbrains.exodus.entitystore.PersistentEntityStores;
import jetbrains.exodus.env.Environments;
Expand All @@ -50,6 +51,8 @@
import org.sonarsource.sonarlint.core.serverconnection.storage.ProtobufFileUtil;

import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.sonarsource.sonarlint.core.serverconnection.storage.XodusServerIssueStore.toProtoFlow;
import static org.sonarsource.sonarlint.core.serverconnection.storage.XodusServerIssueStore.toProtoImpact;

public class ProjectStorageFixture {

Expand Down Expand Up @@ -179,10 +182,15 @@ private void createFindings(Path projectFolder) {
var issuesByFilePath = branch.serverIssues.stream()
.map(ServerIssueFixtures.ServerIssueBuilder::build)
.collect(Collectors.groupingBy(ServerIssueFixtures.ServerIssue::getFilePath));
var taintIssuesByFilePath = branch.serverTaintIssues.stream()
.map(ServerTaintIssueFixtures.ServerTaintIssueBuilder::build)
.collect(Collectors.groupingBy(ServerTaintIssueFixtures.ServerTaintIssue::getFilePath));
var hotspotsByFilePath = branch.serverHotspots.stream()
.map(ServerSecurityHotspotFixture.ServerSecurityHotspotBuilder::build)
.collect(Collectors.groupingBy(ServerSecurityHotspotFixture.ServerHotspot::getFilePath));
concat(issuesByFilePath.keySet(), hotspotsByFilePath.keySet())
Stream.of(issuesByFilePath, taintIssuesByFilePath, hotspotsByFilePath)
.flatMap(map -> map.keySet().stream())
.collect(Collectors.toList())
.forEach(filePath -> {
var fileEntity = txn.newEntity("File");
fileEntity.setProperty("path", filePath);
Expand Down Expand Up @@ -216,6 +224,39 @@ private void createFindings(Path projectFolder) {
fileEntity.addLink("issues", issueEntity);
});

taintIssuesByFilePath.getOrDefault(filePath, Collections.emptyList())
.forEach(taint -> {
var taintIssueEntity = txn.newEntity("TaintIssue");
taintIssueEntity.setProperty("key", taint.key);
taintIssueEntity.setProperty("type", taint.type);
taintIssueEntity.setProperty("resolved", taint.resolved);
taintIssueEntity.setProperty("ruleKey", taint.ruleKey);
taintIssueEntity.setBlobString("message", taint.message);
taintIssueEntity.setProperty("creationDate", taint.creationDate);
if (taint.severity != null) {
taintIssueEntity.setProperty("userSeverity", taint.severity);
}
if (taint.textRange != null) {
var textRange = taint.textRange;
taintIssueEntity.setProperty("startLine", textRange.getStartLine());
taintIssueEntity.setProperty("startLineOffset", textRange.getStartLineOffset());
taintIssueEntity.setProperty("endLine", textRange.getEndLine());
taintIssueEntity.setProperty("endLineOffset", textRange.getEndLineOffset());
taintIssueEntity.setBlobString("rangeHash", textRange.getHash());
}
taintIssueEntity.setBlob("flows", toProtoFlow(taint.flows));
if (taint.ruleDescriptionContextKey != null) {
taintIssueEntity.setProperty("ruleDescriptionContextKey", taint.ruleDescriptionContextKey);
}
if (taint.cleanCodeAttribute != null) {
taintIssueEntity.setProperty("cleanCodeAttribute", taint.cleanCodeAttribute.name());
}
taintIssueEntity.setBlob("impacts", toProtoImpact(taint.impacts));

taintIssueEntity.setLink("file", fileEntity);
fileEntity.addLink("taintIssues", taintIssueEntity);
});

hotspotsByFilePath.getOrDefault(filePath, Collections.emptyList())
.forEach(hotspot -> {
var hotspotEntity = txn.newEntity("Hotspot");
Expand Down Expand Up @@ -280,6 +321,7 @@ public RuleSetBuilder withCustomActiveRule(String ruleKey, String templateKey, S

public static class BranchBuilder {
private final List<ServerIssueFixtures.ServerIssueBuilder> serverIssues = new ArrayList<>();
private final List<ServerTaintIssueFixtures.ServerTaintIssueBuilder> serverTaintIssues = new ArrayList<>();
private final List<ServerSecurityHotspotFixture.ServerSecurityHotspotBuilder> serverHotspots = new ArrayList<>();
private final String name;

Expand All @@ -292,6 +334,11 @@ public BranchBuilder withIssue(ServerIssueFixtures.ServerIssueBuilder serverIssu
return this;
}

public BranchBuilder withTaintIssue(ServerTaintIssueFixtures.ServerTaintIssueBuilder serverTaintIssueBuilder) {
serverTaintIssues.add(serverTaintIssueBuilder);
return this;
}

public BranchBuilder withHotspot(ServerSecurityHotspotFixture.ServerSecurityHotspotBuilder serverHotspotBuilder) {
serverHotspots.add(serverHotspotBuilder);
return this;
Expand Down
Loading

0 comments on commit b528f3e

Please sign in to comment.