Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate all tests to use Postgres testcontainers instead of H2 #573

Merged
merged 9 commits into from
Feb 14, 2024
1 change: 1 addition & 0 deletions .github/workflows/ci-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
- name: Execute unit tests
env:
MAVEN_ARGS: "-B --no-transfer-progress"
TESTCONTAINERS_REUSE_ENABLE: "true"
run: |-
mvn clean
mvn test -P enhance
7 changes: 0 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -402,13 +402,6 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>

<!-- Bundle JDBC drivers -->
<dependency>
<groupId>org.postgresql</groupId>
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/org/dependencytrack/model/Component.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ public enum FetchGroup {

@Persistent
@Index(name = "COMPONENT_CPE_IDX")
@Column(name = "CPE")
@Size(max = 255)
//Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd
@Pattern(regexp = "(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})", message = "The CPE must conform to the CPE v2.2 or v2.3 specification defined by NIST")
Expand All @@ -258,6 +259,7 @@ public enum FetchGroup {
private String purlCoordinates; // Field should contain only type, namespace, name, and version. Everything up to the qualifiers

@Persistent
@Column(name = "SWIDTAGID")
@Index(name = "COMPONENT_SWID_TAGID_IDX")
@Size(max = 255)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The SWID tagId may only contain printable characters")
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/dependencytrack/model/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public enum FetchGroup {

@Persistent
@Index(name = "PROJECT_CPE_IDX")
@Column(name = "CPE")
@Size(max = 255)
@JsonDeserialize(using = TrimmedStringDeserializer.class)
//Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd
Expand All @@ -193,13 +194,15 @@ public enum FetchGroup {

@Persistent
@Index(name = "PROJECT_PURL_IDX")
@Column(name = "PURL")
@Size(max = 255)
@com.github.packageurl.validator.PackageURL
@JsonDeserialize(using = TrimmedStringDeserializer.class)
private String purl;

@Persistent
@Index(name = "PROJECT_SWID_TAGID_IDX")
@Column(name = "SWIDTAGID")
@Size(max = 255)
@JsonDeserialize(using = TrimmedStringDeserializer.class)
@Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The SWID tagId may only contain printable characters")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,7 @@ private List<ProjectVersion> getProjectVersions(Project project) {
query.setFilter("name == :name");
query.setParameters(project.getName());
query.setResult("uuid, version");
query.setOrdering("id asc"); // Ensure consistent ordering
return query.executeResultList(ProjectVersion.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1525,9 +1525,9 @@ public void recursivelyDeleteTeam(Team team) {
pm.currentTransaction().begin();
pm.deletePersistentAll(team.getApiKeys());
String aclDeleteQuery = """
DELETE FROM PROJECT_ACCESS_TEAMS WHERE \"PROJECT_ACCESS_TEAMS\".\"TEAM_ID\" = ?
DELETE FROM "PROJECT_ACCESS_TEAMS" WHERE "TEAM_ID" = ?
""";
final Query query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, aclDeleteQuery);
final Query<?> query = pm.newQuery(JDOQuery.SQL_QUERY_LANGUAGE, aclDeleteQuery);
query.executeWithArray(team.getId());
pm.deletePersistent(team);
pm.currentTransaction().commit();
Expand Down
106 changes: 0 additions & 106 deletions src/test/java/org/dependencytrack/AbstractPostgresEnabledTest.java

This file was deleted.

72 changes: 69 additions & 3 deletions src/test/java/org/dependencytrack/PersistenceCapableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,48 @@
import alpine.Config;
import alpine.server.persistence.PersistenceManagerFactory;
import org.apache.kafka.clients.producer.MockProducer;
import org.datanucleus.PropertyNames;
import org.datanucleus.api.jdo.JDOPersistenceManagerFactory;
import org.dependencytrack.event.kafka.KafkaProducerInitializer;
import org.dependencytrack.persistence.QueryManager;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import org.testcontainers.containers.PostgreSQLContainer;

import javax.jdo.JDOHelper;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;

public abstract class PersistenceCapableTest {

protected QueryManager qm;
@Rule
public EnvironmentVariables environmentVariables = new EnvironmentVariables();

protected static PostgresTestContainer postgresContainer;
protected MockProducer<byte[], byte[]> kafkaMockProducer;
protected QueryManager qm;

@BeforeClass
public static void init() {
Config.enableUnitTests();

postgresContainer = new PostgresTestContainer();
postgresContainer.start();
}

@Before
public void before() throws Exception {
this.qm = new QueryManager();
truncateTables(postgresContainer);
configurePmf(postgresContainer);

qm = new QueryManager();

environmentVariables.set("TASK_PORTFOLIO_REPOMETAANALYSIS_LOCKATLEASTFORINMILLIS", "2000");
this.kafkaMockProducer = (MockProducer<byte[], byte[]>) KafkaProducerInitializer.getProducer();
}

Expand All @@ -50,11 +73,54 @@ public void after() {
// code base can leave such a broken state behind if they run into unexpected
// errors. See: https://github.com/DependencyTrack/dependency-track/issues/2677
if (!qm.getPersistenceManager().isClosed()
&& qm.getPersistenceManager().currentTransaction().isActive()) {
&& qm.getPersistenceManager().currentTransaction().isActive()) {
qm.getPersistenceManager().currentTransaction().rollback();
}

PersistenceManagerFactory.tearDown();
KafkaProducerInitializer.tearDown();
}

@AfterClass
public static void tearDownClass() {
if (postgresContainer != null) {
postgresContainer.stopWhenNotReusing();
}
}

protected static void configurePmf(final PostgreSQLContainer<?> postgresContainer) {
final var dnProps = new Properties();
dnProps.put(PropertyNames.PROPERTY_PERSISTENCE_UNIT_NAME, "Alpine");
dnProps.put(PropertyNames.PROPERTY_SCHEMA_AUTOCREATE_DATABASE, "false");
dnProps.put(PropertyNames.PROPERTY_SCHEMA_AUTOCREATE_TABLES, "false");
dnProps.put(PropertyNames.PROPERTY_SCHEMA_AUTOCREATE_COLUMNS, "false");
dnProps.put(PropertyNames.PROPERTY_SCHEMA_AUTOCREATE_CONSTRAINTS, "false");
dnProps.put("datanucleus.schema.generatedatabase.mode", "none");
dnProps.put("datanucleus.query.jdoql.allowall", "true");
dnProps.put(PropertyNames.PROPERTY_CONNECTION_URL, postgresContainer.getJdbcUrl());
dnProps.put(PropertyNames.PROPERTY_CONNECTION_DRIVER_NAME, postgresContainer.getDriverClassName());
dnProps.put(PropertyNames.PROPERTY_CONNECTION_USER_NAME, postgresContainer.getUsername());
dnProps.put(PropertyNames.PROPERTY_CONNECTION_PASSWORD, postgresContainer.getPassword());

final var pmf = (JDOPersistenceManagerFactory) JDOHelper.getPersistenceManagerFactory(dnProps, "Alpine");
PersistenceManagerFactory.setJdoPersistenceManagerFactory(pmf);
}

protected static void truncateTables(final PostgreSQLContainer<?> postgresContainer) throws Exception {
// Truncate all tables to ensure each test starts from a clean slate.
// https://stackoverflow.com/a/63227261
try (final Connection connection = postgresContainer.createConnection("");
final Statement statement = connection.createStatement()) {
statement.execute("""
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (SELECT tablename FROM pg_tables WHERE schemaname = CURRENT_SCHEMA()) LOOP
EXECUTE 'TRUNCATE TABLE ' || QUOTE_IDENT(r.tablename) || ' CASCADE';
END LOOP;
END $$;
""");
}
}

}
53 changes: 53 additions & 0 deletions src/test/java/org/dependencytrack/PostgresTestContainer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.dependencytrack;

import com.github.dockerjava.api.command.InspectContainerResponse;
import org.dependencytrack.persistence.migration.MigrationInitializer;
import org.postgresql.ds.PGSimpleDataSource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
import org.testcontainers.utility.TestcontainersConfiguration;

public class PostgresTestContainer extends PostgreSQLContainer<PostgresTestContainer> {

@SuppressWarnings("resource")
public PostgresTestContainer() {
super(DockerImageName.parse("postgres:11-alpine"));
withUsername("dtrack");
withPassword("dtrack");
withDatabaseName("dtrack");
withLabel("owner", "hyades-apiserver");

// NB: Container reuse won't be active unless either:
// - The environment variable TESTCONTAINERS_REUSE_ENABLE=true is set
// - testcontainers.reuse.enable=false is set in ~/.testcontainers.properties
withReuse(true);
}

@Override
protected void containerIsStarted(final InspectContainerResponse containerInfo, final boolean reused) {
super.containerIsStarted(containerInfo, reused);

if (reused) {
logger().debug("Reusing container; Migration not necessary");
return;
}

final var dataSource = new PGSimpleDataSource();
dataSource.setUrl(getJdbcUrl());
dataSource.setUser(getUsername());
dataSource.setPassword(getPassword());

try {
MigrationInitializer.runMigration(dataSource, /* silent */ true);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public void stopWhenNotReusing() {
if (!TestcontainersConfiguration.getInstance().environmentSupportsReuse() || !isShouldBeReused()) {
stop();
}
}

}
Loading
Loading