91 changed files with 3104 additions and 993 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 |
config.stopbubbling = true |
||||
lombok.anyconstructor.addconstructorproperties = 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> |
||||
Loading…
Reference in new issue