145 changed files with 3963 additions and 1165 deletions
@ -0,0 +1,162 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.jwt.settings; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.cluster.TbClusterService; |
|||
import org.thingsboard.server.common.data.AdminSettings; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
import org.thingsboard.server.common.data.security.model.JwtSettings; |
|||
import org.thingsboard.server.dao.settings.AdminSettingsService; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.Base64; |
|||
import java.util.Objects; |
|||
import java.util.Optional; |
|||
|
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class DefaultJwtSettingsService implements JwtSettingsService { |
|||
|
|||
@Lazy |
|||
private final AdminSettingsService adminSettingsService; |
|||
@Lazy |
|||
private final Optional<TbClusterService> tbClusterService; |
|||
private final JwtSettingsValidator jwtSettingsValidator; |
|||
|
|||
@Value("${security.jwt.tokenExpirationTime:9000}") |
|||
private Integer tokenExpirationTime; |
|||
@Value("${security.jwt.refreshTokenExpTime:604800}") |
|||
private Integer refreshTokenExpTime; |
|||
@Value("${security.jwt.tokenIssuer:thingsboard.io}") |
|||
private String tokenIssuer; |
|||
@Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") |
|||
private String tokenSigningKey; |
|||
|
|||
private volatile JwtSettings jwtSettings = null; //lazy init
|
|||
|
|||
/** |
|||
* Create JWT admin settings is intended to be called from Install scripts only |
|||
*/ |
|||
@Override |
|||
public void createRandomJwtSettings() { |
|||
if (getJwtSettingsFromDb() == null) { |
|||
log.info("Creating JWT admin settings..."); |
|||
this.jwtSettings = getJwtSettingsFromYml(); |
|||
if (isSigningKeyDefault(jwtSettings)) { |
|||
this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( |
|||
RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); |
|||
} |
|||
saveJwtSettings(jwtSettings); |
|||
} else { |
|||
log.info("Skip creating JWT admin settings because they already exist."); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Create JWT admin settings is intended to be called from Upgrade scripts only |
|||
*/ |
|||
@Override |
|||
public void saveLegacyYmlSettings() { |
|||
log.info("Saving legacy JWT admin settings from YML..."); |
|||
if (getJwtSettingsFromDb() == null) { |
|||
saveJwtSettings(getJwtSettingsFromYml()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { |
|||
jwtSettingsValidator.validate(jwtSettings); |
|||
final AdminSettings adminJwtSettings = mapJwtToAdminSettings(jwtSettings); |
|||
final AdminSettings existedSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); |
|||
if (existedSettings != null) { |
|||
adminJwtSettings.setId(existedSettings.getId()); |
|||
} |
|||
|
|||
log.info("Saving new JWT admin settings. From this moment, the JWT parameters from YAML and ENV will be ignored"); |
|||
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminJwtSettings); |
|||
|
|||
tbClusterService.ifPresent(cs -> cs.broadcastEntityStateChangeEvent(TenantId.SYS_TENANT_ID, TenantId.SYS_TENANT_ID, ComponentLifecycleEvent.UPDATED)); |
|||
return reloadJwtSettings(); |
|||
} |
|||
|
|||
@Override |
|||
public JwtSettings reloadJwtSettings() { |
|||
return getJwtSettings(true); |
|||
} |
|||
|
|||
@Override |
|||
public JwtSettings getJwtSettings() { |
|||
return getJwtSettings(false); |
|||
} |
|||
|
|||
public JwtSettings getJwtSettings(boolean forceReload) { |
|||
if (this.jwtSettings == null || forceReload) { |
|||
synchronized (this) { |
|||
if (this.jwtSettings == null || forceReload) { |
|||
JwtSettings result = getJwtSettingsFromDb(); |
|||
if (result == null) { |
|||
result = getJwtSettingsFromYml(); |
|||
log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied."); |
|||
} |
|||
if (isSigningKeyDefault(result)) { |
|||
log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + |
|||
"This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + |
|||
"Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); |
|||
} |
|||
this.jwtSettings = result; |
|||
} |
|||
} |
|||
} |
|||
return this.jwtSettings; |
|||
} |
|||
|
|||
private JwtSettings getJwtSettingsFromYml() { |
|||
return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); |
|||
} |
|||
|
|||
private JwtSettings getJwtSettingsFromDb() { |
|||
AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); |
|||
return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null; |
|||
} |
|||
|
|||
private JwtSettings mapAdminToJwtSettings(AdminSettings adminSettings) { |
|||
Objects.requireNonNull(adminSettings, "adminSettings for JWT is null"); |
|||
return JacksonUtil.treeToValue(adminSettings.getJsonValue(), JwtSettings.class); |
|||
} |
|||
|
|||
private AdminSettings mapJwtToAdminSettings(JwtSettings jwtSettings) { |
|||
Objects.requireNonNull(jwtSettings, "jwtSettings is null"); |
|||
AdminSettings adminJwtSettings = new AdminSettings(); |
|||
adminJwtSettings.setTenantId(TenantId.SYS_TENANT_ID); |
|||
adminJwtSettings.setKey(ADMIN_SETTINGS_JWT_KEY); |
|||
adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(jwtSettings)); |
|||
return adminJwtSettings; |
|||
} |
|||
|
|||
private boolean isSigningKeyDefault(JwtSettings settings) { |
|||
return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.jwt.settings; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.apache.commons.lang3.RandomUtils; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.bouncycastle.util.Arrays; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.security.model.JwtSettings; |
|||
import org.thingsboard.server.dao.exception.DataValidationException; |
|||
|
|||
import java.util.Base64; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class DefaultJwtSettingsValidator implements JwtSettingsValidator { |
|||
|
|||
@Override |
|||
public void validate(JwtSettings jwtSettings) { |
|||
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) { |
|||
throw new DataValidationException("JWT token issuer should be specified!"); |
|||
} |
|||
if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) { |
|||
throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!"); |
|||
} |
|||
if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) { |
|||
throw new DataValidationException("JWT token expiration time should be at least 1 minute!"); |
|||
} |
|||
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) { |
|||
throw new DataValidationException("JWT token expiration time should greater than JWT refresh token expiration time!"); |
|||
} |
|||
if (StringUtils.isEmpty(jwtSettings.getTokenSigningKey())) { |
|||
throw new DataValidationException("JWT token signing key should be specified!"); |
|||
} |
|||
|
|||
byte[] decodedKey; |
|||
try { |
|||
decodedKey = Base64.getDecoder().decode(jwtSettings.getTokenSigningKey()); |
|||
} catch (Exception e) { |
|||
throw new DataValidationException("JWT token signing key should be a valid Base64 encoded string! " + e.getMessage()); |
|||
} |
|||
|
|||
if (Arrays.isNullOrEmpty(decodedKey)) { |
|||
throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); |
|||
} |
|||
if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { |
|||
throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); |
|||
} |
|||
|
|||
System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.jwt.settings; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.context.annotation.Primary; |
|||
import org.springframework.context.annotation.Profile; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.security.model.JwtSettings; |
|||
|
|||
/** |
|||
* During Install or upgrade the validation is suppressed to keep existing data |
|||
* */ |
|||
|
|||
@Primary |
|||
@Profile("install") |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class InstallJwtSettingsValidator implements JwtSettingsValidator { |
|||
|
|||
@Override |
|||
public void validate(JwtSettings jwtSettings) { |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.jwt.settings; |
|||
|
|||
import org.thingsboard.server.common.data.security.model.JwtSettings; |
|||
|
|||
public interface JwtSettingsService { |
|||
|
|||
String ADMIN_SETTINGS_JWT_KEY = "jwt"; |
|||
String TOKEN_SIGNING_KEY_DEFAULT = "thingsboardDefaultSigningKey"; |
|||
|
|||
JwtSettings getJwtSettings(); |
|||
|
|||
JwtSettings reloadJwtSettings(); |
|||
|
|||
void createRandomJwtSettings(); |
|||
|
|||
void saveLegacyYmlSettings(); |
|||
|
|||
JwtSettings saveJwtSettings(JwtSettings jwtSettings); |
|||
|
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.jwt.settings; |
|||
|
|||
import org.thingsboard.server.common.data.security.model.JwtSettings; |
|||
|
|||
public interface JwtSettingsValidator { |
|||
|
|||
void validate(JwtSettings jwtSettings); |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.security.auth.oauth2; |
|||
|
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.mockito.Mock; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.id.UserId; |
|||
import org.thingsboard.server.common.data.security.model.JwtPair; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.service.security.model.SecurityUser; |
|||
import org.thingsboard.server.service.security.model.token.JwtTokenFactory; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@DaoSqlTest |
|||
public class Oauth2AuthenticationSuccessHandlerTest extends AbstractControllerTest { |
|||
|
|||
@Autowired |
|||
private Oauth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; |
|||
|
|||
@Mock |
|||
private JwtTokenFactory jwtTokenFactory; |
|||
|
|||
private SecurityUser securityUser; |
|||
|
|||
@Before |
|||
public void before() { |
|||
UserId userId = new UserId(UUID.randomUUID()); |
|||
securityUser = new SecurityUser(userId); |
|||
when(jwtTokenFactory.createTokenPair(eq(securityUser))).thenReturn(new JwtPair("testAccessToken", "testRefreshToken")); |
|||
} |
|||
|
|||
@Test |
|||
public void testGetRedirectUrl() { |
|||
JwtPair jwtPair = jwtTokenFactory.createTokenPair(securityUser); |
|||
|
|||
String urlWithoutParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e"; |
|||
String urlWithParams = "http://localhost:8080/dashboardGroups/3fa13530-6597-11ed-bd76-8bd591f0ec3e?state=someState&page=1"; |
|||
|
|||
String redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithoutParams, jwtPair); |
|||
String expectedUrl = urlWithoutParams + "/?accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken(); |
|||
assertEquals(expectedUrl, redirectUrl); |
|||
|
|||
redirectUrl = oauth2AuthenticationSuccessHandler.getRedirectUrl(urlWithParams, jwtPair); |
|||
expectedUrl = urlWithParams + "&accessToken=" + jwtPair.getToken() + "&refreshToken=" + jwtPair.getRefreshToken(); |
|||
assertEquals(expectedUrl, redirectUrl); |
|||
} |
|||
} |
|||
@ -0,0 +1,133 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=DAYS", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningDaysAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsDays() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("DAYS"); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:01Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T00:00:00Z").getTime()); |
|||
} |
|||
|
|||
@Test |
|||
public void testCalculatePartitionsDays() throws ParseException { |
|||
long startTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()); |
|||
long nextTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T23:59:59Z").getTime()); |
|||
long endTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime()); |
|||
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs); |
|||
|
|||
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L)); |
|||
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L)); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime())); |
|||
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(6).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-12T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-13T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-14T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-15T00:00:00Z").getTime())); |
|||
|
|||
long leapStartTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime()); |
|||
long leapEndTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.calculatePartitions(leapStartTs, leapEndTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-27T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-28T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime())); |
|||
|
|||
long newYearStartTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime()); |
|||
long newYearEndTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.calculatePartitions(newYearStartTs, newYearEndTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-30T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-31T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=HOURS", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningHoursAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsHours() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("HOURS"); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T01:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:01Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T02:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:00:00Z").getTime()); |
|||
} |
|||
|
|||
@Test |
|||
public void testCalculatePartitionsHours() throws ParseException { |
|||
long startTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()); |
|||
long nextTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:59:59Z").getTime()); |
|||
long endTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:59:00Z").getTime()); |
|||
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs); |
|||
|
|||
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L)); |
|||
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L)); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime())); |
|||
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(25).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T01:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T02:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T03:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T04:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T05:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T06:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T07:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T08:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T09:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T10:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T11:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T12:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T13:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T14:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T15:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T16:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T17:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T18:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T19:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T20:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T21:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T22:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T23:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-11T00:00:00Z").getTime())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Ignore; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=INDEFINITE", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningIndefiniteAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsIndefinite() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("INDEFINITE"); |
|||
assertThat(tsDao.toPartitionTs(ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(0L); |
|||
} |
|||
|
|||
|
|||
@Test |
|||
public void testCalculatePartitionsIndefinite() throws ParseException { |
|||
//Indefinite partitioning should never call tsDao.calculatePartitions()
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,121 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Ignore; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=MINUTES", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsMinutes() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("MINUTES"); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-02T00:01:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:01Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-03T00:02:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:00Z").getTime()); |
|||
} |
|||
|
|||
|
|||
@Test |
|||
public void testCalculatePartitionsMinutes() throws ParseException { |
|||
long startTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime()); |
|||
long nextTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:59Z").getTime()); |
|||
long endTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime()); |
|||
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs); |
|||
|
|||
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L)); |
|||
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L)); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime())); |
|||
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(11).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:01:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:02:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:03:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:04:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:05:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:06:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:07:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:08:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:09:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=MONTHS", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsMonths() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("MONTHS"); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo(1640995200000L).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo(1651363200000L).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo(1651363200000L).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo(1651363200000L).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo(1701388800000L).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-01T00:00:00Z").getTime()); |
|||
} |
|||
|
|||
@Test |
|||
public void testCalculatePartitionsMonths() throws ParseException { |
|||
long startTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); |
|||
long nextTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-31T23:59:59Z").getTime()); |
|||
long leapTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-29T23:59:59Z").getTime()); |
|||
long endTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); |
|||
log.info("startTs {}, nextTs {}, leapTs {}, endTs {}", startTs, nextTs, leapTs, endTs); |
|||
|
|||
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L)); |
|||
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L)); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of(1575158400000L)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime())); |
|||
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of(1575158400000L, 1577836800000L)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, leapTs)).isEqualTo(List.of(1575158400000L, 1577836800000L, 1580515200000L)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(14).isEqualTo(List.of( |
|||
1575158400000L, |
|||
1577836800000L, 1580515200000L, 1583020800000L, |
|||
1585699200000L, 1588291200000L, 1590969600000L, |
|||
1593561600000L, 1596240000000L, 1598918400000L, |
|||
1601510400000L, 1604188800000L, 1606780800000L, |
|||
1609459200000L)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-02-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-03-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-04-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-05-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-06-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-07-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-08-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-09-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-10-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-11-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-12-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime())); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.dao.timeseries; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Ignore; |
|||
import org.junit.Test; |
|||
import org.junit.runner.RunWith; |
|||
import org.mockito.Answers; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.beans.factory.annotation.Qualifier; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.test.mock.mockito.MockBean; |
|||
import org.springframework.test.context.TestPropertySource; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.thingsboard.server.dao.cassandra.CassandraCluster; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; |
|||
import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; |
|||
|
|||
import java.text.ParseException; |
|||
import java.util.List; |
|||
|
|||
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; |
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
|
|||
@RunWith(SpringRunner.class) |
|||
@SpringBootTest(classes = CassandraBaseTimeseriesDao.class) |
|||
@TestPropertySource(properties = { |
|||
"database.ts.type=cassandra", |
|||
"cassandra.query.ts_key_value_partitioning=YEARS", |
|||
"cassandra.query.use_ts_key_value_partitioning_on_read=false", |
|||
"cassandra.query.ts_key_value_partitions_max_cache_size=100000", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_enabled=true", |
|||
"cassandra.query.ts_key_value_partitions_cache_stats_interval=60", |
|||
"cassandra.query.ts_key_value_ttl=0", |
|||
"cassandra.query.set_null_values_enabled=false", |
|||
}) |
|||
@Slf4j |
|||
public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest { |
|||
|
|||
@Autowired |
|||
CassandraBaseTimeseriesDao tsDao; |
|||
|
|||
@MockBean(answer = Answers.RETURNS_MOCKS) |
|||
@Qualifier("CassandraCluster") |
|||
CassandraCluster cassandraCluster; |
|||
|
|||
@MockBean |
|||
CassandraBufferedRateReadExecutor cassandraBufferedRateReadExecutor; |
|||
@MockBean |
|||
CassandraBufferedRateWriteExecutor cassandraBufferedRateWriteExecutor; |
|||
|
|||
@Test |
|||
public void testToPartitionsYears() throws ParseException { |
|||
assertThat(tsDao.getPartitioning()).isEqualTo("YEARS"); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:00Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-01T00:00:01Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-05-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime()); |
|||
assertThat(tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-12-31T23:59:59Z").getTime())).isEqualTo( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime()); |
|||
} |
|||
|
|||
@Test |
|||
public void testCalculatePartitionsYears() throws ParseException { |
|||
long startTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime()); |
|||
long nextTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-10-12T23:59:59Z").getTime()); |
|||
long endTs = tsDao.toPartitionTs( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-07-15T00:00:00Z").getTime()); |
|||
log.info("startTs {}, nextTs {}, endTs {}", startTs, nextTs, endTs); |
|||
|
|||
assertThat(tsDao.calculatePartitions(0, 0)).isEqualTo(List.of(0L)); |
|||
assertThat(tsDao.calculatePartitions(0, 1)).isEqualTo(List.of(0L, 1L)); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, startTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime())); |
|||
assertThat(tsDao.calculatePartitions(startTs, nextTs)).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime())); |
|||
|
|||
assertThat(tsDao.calculatePartitions(startTs, endTs)).hasSize(7).isEqualTo(List.of( |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2019-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2020-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2022-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2023-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2024-01-01T00:00:00Z").getTime(), |
|||
ISO_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime())); |
|||
} |
|||
|
|||
} |
|||
@ -1,2 +1,3 @@ |
|||
config.stopbubbling = true |
|||
lombok.anyconstructor.addconstructorproperties = true |
|||
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy |
|||
|
|||
@ -0,0 +1,53 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.testng.ITestContext; |
|||
import org.testng.ITestResult; |
|||
import org.testng.TestListenerAdapter; |
|||
|
|||
import static org.testng.internal.Utils.log; |
|||
|
|||
@Slf4j |
|||
public class TestListener extends TestListenerAdapter { |
|||
|
|||
@Override |
|||
public void onTestStart(ITestResult result) { |
|||
super.onTestStart(result); |
|||
log.info("===>>> Test started: " + result.getName()); |
|||
} |
|||
|
|||
/** |
|||
* Invoked when a test succeeds |
|||
*/ |
|||
@Override |
|||
public void onTestSuccess(ITestResult result) { |
|||
super.onTestSuccess(result); |
|||
if (result != null) { |
|||
log.info("<<<=== Test completed successfully: " + result.getName()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Invoked when a test fails |
|||
*/ |
|||
@Override |
|||
public void onTestFailure(ITestResult result) { |
|||
super.onTestFailure(result); |
|||
log.info("<<<=== Test failed: " + result.getName()); |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
import java.util.Properties; |
|||
|
|||
@Slf4j |
|||
public class TestProperties { |
|||
|
|||
private static final String HTTPS_URL = "https://localhost"; |
|||
|
|||
private static final String WSS_URL = "wss://localhost"; |
|||
|
|||
private static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); |
|||
|
|||
private static Properties properties; |
|||
|
|||
public static String getBaseUrl() { |
|||
if (instance.isActive()) { |
|||
return HTTPS_URL; |
|||
} |
|||
return getProperties().getProperty("tb.baseUrl"); |
|||
} |
|||
|
|||
public static String getWebSocketUrl() { |
|||
if (instance.isActive()) { |
|||
return WSS_URL; |
|||
} |
|||
return getProperties().getProperty("tb.wsUrl"); |
|||
} |
|||
|
|||
private static Properties getProperties() { |
|||
if (properties == null) { |
|||
try (InputStream input = TestProperties.class.getClassLoader().getResourceAsStream("config.properties")) { |
|||
properties = new Properties(); |
|||
properties.load(input); |
|||
} catch (IOException ex) { |
|||
log.error("Exception while reading test properties " + ex.getMessage()); |
|||
} |
|||
} |
|||
return properties; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,262 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import io.restassured.RestAssured; |
|||
import io.restassured.common.mapper.TypeRef; |
|||
import io.restassured.config.HeaderConfig; |
|||
import io.restassured.config.RestAssuredConfig; |
|||
import io.restassured.filter.log.RequestLoggingFilter; |
|||
import io.restassured.filter.log.ResponseLoggingFilter; |
|||
import io.restassured.http.ContentType; |
|||
import io.restassured.path.json.JsonPath; |
|||
import io.restassured.response.ValidatableResponse; |
|||
import io.restassured.specification.RequestSpecification; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.RuleChainId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.relation.EntityRelation; |
|||
import org.thingsboard.server.common.data.relation.RelationTypeGroup; |
|||
import org.thingsboard.server.common.data.rule.RuleChain; |
|||
import org.thingsboard.server.common.data.rule.RuleChainMetaData; |
|||
import org.thingsboard.server.common.data.security.DeviceCredentials; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
import static io.restassured.RestAssured.given; |
|||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND; |
|||
import static java.net.HttpURLConnection.HTTP_OK; |
|||
import static org.hamcrest.Matchers.is; |
|||
import static org.hamcrest.core.AnyOf.anyOf; |
|||
import static org.thingsboard.server.common.data.StringUtils.isEmpty; |
|||
|
|||
public class TestRestClient { |
|||
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; |
|||
private static final String CONTENT_TYPE_HEADER = "Content-Type"; |
|||
private final RequestSpecification requestSpec; |
|||
private String token; |
|||
private String refreshToken; |
|||
|
|||
public TestRestClient(String url) { |
|||
RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); |
|||
|
|||
requestSpec = given().baseUri(url) |
|||
.contentType(ContentType.JSON) |
|||
.config(RestAssuredConfig.config() |
|||
.headerConfig(HeaderConfig.headerConfig() |
|||
.overwriteHeadersWithName(JWT_TOKEN_HEADER_PARAM, CONTENT_TYPE_HEADER))); |
|||
|
|||
if (url.matches("^(https)://.*$")) { |
|||
requestSpec.relaxedHTTPSValidation(); |
|||
} |
|||
} |
|||
|
|||
public void login(String username, String password) { |
|||
Map<String, String> loginRequest = new HashMap<>(); |
|||
loginRequest.put("username", username); |
|||
loginRequest.put("password", password); |
|||
|
|||
JsonPath jsonPath = given().spec(requestSpec).body(loginRequest) |
|||
.post( "/api/auth/login") |
|||
.getBody().jsonPath(); |
|||
token = jsonPath.get("token"); |
|||
refreshToken = jsonPath.get("refreshToken"); |
|||
requestSpec.header(JWT_TOKEN_HEADER_PARAM, "Bearer " + token); |
|||
} |
|||
|
|||
public Device postDevice(String accessToken, Device device) { |
|||
return given().spec(requestSpec).body(device) |
|||
.pathParams("accessToken", accessToken) |
|||
.post("/api/device?accessToken={accessToken}") |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(Device.class); |
|||
} |
|||
|
|||
public ValidatableResponse getDeviceById(DeviceId deviceId, int statusCode) { |
|||
return given().spec(requestSpec) |
|||
.pathParams("deviceId", deviceId.getId()) |
|||
.get("/api/device/{deviceId}") |
|||
.then() |
|||
.statusCode(statusCode); |
|||
} |
|||
public Device getDeviceById(DeviceId deviceId) { |
|||
return getDeviceById(deviceId, HTTP_OK) |
|||
.extract() |
|||
.as(Device.class); |
|||
} |
|||
public DeviceCredentials getDeviceCredentialsByDeviceId(DeviceId deviceId) { |
|||
return given().spec(requestSpec).get("/api/device/{deviceId}/credentials", deviceId.getId()) |
|||
.then() |
|||
.assertThat() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(DeviceCredentials.class); |
|||
} |
|||
|
|||
public ValidatableResponse postTelemetry(String credentialsId, JsonNode telemetry) { |
|||
return given().spec(requestSpec).body(telemetry) |
|||
.post("/api/v1/{credentialsId}/telemetry", credentialsId) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
|
|||
public ValidatableResponse deleteDevice(DeviceId deviceId) { |
|||
return given().spec(requestSpec) |
|||
.delete("/api/device/{deviceId}", deviceId.getId()) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
public ValidatableResponse deleteDeviceIfExists(DeviceId deviceId) { |
|||
return given().spec(requestSpec) |
|||
.delete("/api/device/{deviceId}", deviceId.getId()) |
|||
.then() |
|||
.statusCode(anyOf(is(HTTP_OK),is(HTTP_NOT_FOUND))); |
|||
} |
|||
|
|||
public ValidatableResponse postTelemetryAttribute(String entityType, DeviceId deviceId, String scope, JsonNode attribute) { |
|||
return given().spec(requestSpec).body(attribute) |
|||
.post("/api/plugins/telemetry/{entityType}/{entityId}/attributes/{scope}", entityType, deviceId.getId(), scope) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
|
|||
public ValidatableResponse postAttribute(String accessToken, JsonNode attribute) { |
|||
return given().spec(requestSpec).body(attribute) |
|||
.post("/api/v1/{accessToken}/attributes/", accessToken) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
|
|||
public JsonNode getAttributes(String accessToken, String clientKeys, String sharedKeys) { |
|||
return given().spec(requestSpec) |
|||
.queryParam("clientKeys", clientKeys) |
|||
.queryParam("sharedKeys", sharedKeys) |
|||
.get("/api/v1/{accessToken}/attributes", accessToken) |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(JsonNode.class); |
|||
} |
|||
|
|||
public PageData<RuleChain> getRuleChains(PageLink pageLink) { |
|||
Map<String, String> params = new HashMap<>(); |
|||
addPageLinkToParam(params, pageLink); |
|||
return given().spec(requestSpec).queryParams(params) |
|||
.get("/api/ruleChains") |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(new TypeRef<PageData<RuleChain>>() {}); |
|||
} |
|||
|
|||
public RuleChain postRootRuleChain(RuleChain ruleChain) { |
|||
return given().spec(requestSpec) |
|||
.body(ruleChain) |
|||
.post("/api/ruleChain") |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(RuleChain.class); |
|||
} |
|||
|
|||
public RuleChainMetaData postRuleChainMetadata(RuleChainMetaData ruleChainMetaData) { |
|||
return given().spec(requestSpec) |
|||
.body(ruleChainMetaData) |
|||
.post("/api/ruleChain/metadata") |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(RuleChainMetaData.class); |
|||
} |
|||
|
|||
public void setRootRuleChain(RuleChainId ruleChainId) { |
|||
given().spec(requestSpec) |
|||
.post("/api/ruleChain/{ruleChainId}/root", ruleChainId.getId()) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
|
|||
public void deleteRuleChain(RuleChainId ruleChainId) { |
|||
given().spec(requestSpec) |
|||
.delete("/api/ruleChain/{ruleChainId}", ruleChainId.getId()) |
|||
.then() |
|||
.statusCode(HTTP_OK); |
|||
} |
|||
|
|||
private String getUrlParams(PageLink pageLink) { |
|||
String urlParams = "pageSize={pageSize}&page={page}"; |
|||
if (!isEmpty(pageLink.getTextSearch())) { |
|||
urlParams += "&textSearch={textSearch}"; |
|||
} |
|||
if (pageLink.getSortOrder() != null) { |
|||
urlParams += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; |
|||
} |
|||
return urlParams; |
|||
} |
|||
|
|||
private void addPageLinkToParam(Map<String, String> params, PageLink pageLink) { |
|||
params.put("pageSize", String.valueOf(pageLink.getPageSize())); |
|||
params.put("page", String.valueOf(pageLink.getPage())); |
|||
if (!isEmpty(pageLink.getTextSearch())) { |
|||
params.put("textSearch", pageLink.getTextSearch()); |
|||
} |
|||
if (pageLink.getSortOrder() != null) { |
|||
params.put("sortProperty", pageLink.getSortOrder().getProperty()); |
|||
params.put("sortOrder", pageLink.getSortOrder().getDirection().name()); |
|||
} |
|||
} |
|||
|
|||
public List<EntityRelation> findRelationByFrom(EntityId fromId, RelationTypeGroup relationTypeGroup) { |
|||
Map<String, String> params = new HashMap<>(); |
|||
params.put("fromId", fromId.getId().toString()); |
|||
params.put("fromType", fromId.getEntityType().name()); |
|||
params.put("relationTypeGroup", relationTypeGroup.name()); |
|||
|
|||
return given().spec(requestSpec) |
|||
.pathParams(params) |
|||
.get("/api/relations?fromId={fromId}&fromType={fromType}&relationTypeGroup={relationTypeGroup}") |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(new TypeRef<List<EntityRelation>>() {}); |
|||
} |
|||
|
|||
public JsonNode postServerSideRpc(DeviceId deviceId, JsonNode serverRpcPayload) { |
|||
return given().spec(requestSpec) |
|||
.body(serverRpcPayload) |
|||
.post("/api/rpc/twoway/{deviceId}", deviceId.getId()) |
|||
.then() |
|||
.statusCode(HTTP_OK) |
|||
.extract() |
|||
.as(JsonNode.class); |
|||
} |
|||
|
|||
public String getToken() { |
|||
return token; |
|||
} |
|||
|
|||
public String getRefreshToken() { |
|||
return refreshToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2022 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa.prototypes; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.Device; |
|||
|
|||
public class DevicePrototypes { |
|||
public static Device defaultDevicePrototype(String name){ |
|||
Device device = new Device(); |
|||
device.setName(name + RandomStringUtils.randomAlphanumeric(7)); |
|||
device.setType("DEFAULT"); |
|||
return device; |
|||
} |
|||
|
|||
public static Device defaultGatewayPrototype() { |
|||
String isGateway = "{\"gateway\":true}"; |
|||
JsonNode additionalInfo = JacksonUtil.toJsonNode(isGateway); |
|||
Device gatewayDeviceTemplate = new Device(); |
|||
gatewayDeviceTemplate.setName("mqtt_gateway_" + RandomStringUtils.randomAlphanumeric(5)); |
|||
gatewayDeviceTemplate.setType("gateway"); |
|||
gatewayDeviceTemplate.setAdditionalInfo(additionalInfo); |
|||
return gatewayDeviceTemplate; |
|||
} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
tb.baseUrl=http://localhost:8080 |
|||
tb.wsUrl=ws://localhost:8080 |
|||
@ -0,0 +1,27 @@ |
|||
<?xml version="1.0" encoding="ISO-8859-1"?> |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd"> |
|||
|
|||
<suite name="Black-box tests"> |
|||
<test verbose="2" name="Connectivity tests" preserve-order="false"> |
|||
<packages> |
|||
<package name="org.thingsboard.server.msa.connectivity" /> |
|||
</packages> |
|||
</test> |
|||
</suite> |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue