diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 241f324..11dc954 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -6,7 +6,6 @@ jobs: build-test: name: Build & Test runs-on: ubuntu-latest - # runs-on: gradle:7.5.1-jdk8 services: postgres: image: postgres:15 @@ -19,8 +18,15 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 - - run: ./gradlew test + - uses: actions/checkout@v4 + - name: setup java + uses: actions/setup-java@4 + with: + java-version: 17 + distribution: temurin + cache: gradle + - name: Run tests + run: ./gradlew test - name: Test Report uses: dorny/test-reporter@v1 if: success() || failure() # run this step even if previous step failed diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaConfiguration.java b/src/main/java/net/ripe/rpki/domain/roa/RoaConfiguration.java index b4a8870..942c9b0 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaConfiguration.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaConfiguration.java @@ -1,47 +1,49 @@ package net.ripe.rpki.domain.roa; import com.google.common.base.Preconditions; +import jakarta.persistence.*; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.ripe.ipresource.Asn; import net.ripe.ipresource.ImmutableResourceSet; +import net.ripe.ipresource.IpRange; import net.ripe.rpki.commons.crypto.ValidityPeriod; -import net.ripe.rpki.commons.validation.roa.AnnouncedRoute; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.domain.IncomingResourceCertificate; import net.ripe.rpki.ncc.core.domain.support.EntitySupport; import net.ripe.rpki.server.api.dto.RoaConfigurationData; - -import jakarta.persistence.CollectionTable; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; -import jakarta.persistence.SequenceGenerator; -import jakarta.persistence.Table; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; +import net.ripe.rpki.util.Streams; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; + +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Comparator.*; /** * Specification for a ROA. This specification determines how ROAs must be * created and managed. The ROA specification can be edited by the user. The * system will then take care of managing the required ROAs. */ +@DynamicInsert +@DynamicUpdate @Slf4j @Entity @Table(name = "roaconfiguration") @SequenceGenerator(name = "seq_roaconfiguration", sequenceName = "seq_all", allocationSize = 1) public class RoaConfiguration extends EntitySupport { + public static final Comparator ROA_CONFIGURATION_PREFIX_COMPARATOR = + comparing(RoaConfigurationPrefix::getAsn) + .thenComparing(RoaConfigurationPrefix::getPrefix) + .thenComparing(RoaConfigurationPrefix::getMaximumLength, reverseOrder()) + .thenComparing(RoaConfigurationPrefix::getUpdatedAt, nullsLast(naturalOrder())); + @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_roaconfiguration") @Getter @@ -49,10 +51,10 @@ public class RoaConfiguration extends EntitySupport { @OneToOne(optional = false) @JoinColumn(name = "certificateauthority_id") + @Getter private ManagedCertificateAuthority certificateAuthority; - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "roaconfiguration_prefixes", joinColumns = @JoinColumn(name = "roaconfiguration_id")) + @OneToMany(fetch = FetchType.EAGER, mappedBy = "roaConfiguration", cascade = CascadeType.MERGE) private Set prefixes = new HashSet<>(); public RoaConfiguration() { @@ -67,31 +69,25 @@ public RoaConfiguration(ManagedCertificateAuthority certificateAuthority, Collec this.prefixes.addAll(prefixes); } - public void setPrefixes(Collection prefixes) { - this.prefixes = convertToSet(convertToMap(prefixes)); + public void setPrefixes(Collection prefixes) { + this.prefixes = canonicalRoaPrefixes(prefixes.stream()); } public Set getPrefixes() { return Collections.unmodifiableSet(prefixes); } - public ManagedCertificateAuthority getCertificateAuthority() { - return certificateAuthority; - } - public RoaConfigurationData convertToData() { return new RoaConfigurationData(prefixes.stream() .map(RoaConfigurationPrefix::toData).toList()); } - public final void addPrefix(Collection roaPrefixes) { - Map byPrefix = convertToMap(prefixes); - byPrefix.putAll(convertToMap(roaPrefixes)); - prefixes = convertToSet(byPrefix); + public final PrefixDiff addPrefixes(Collection roaPrefixes) { + return mergePrefixes(roaPrefixes, Collections.emptyList()); } - public final void removePrefix(Collection roaPrefixes) { - prefixes.removeAll(roaPrefixes); + public final PrefixDiff removePrefixes(Collection roaPrefixes) { + return mergePrefixes(Collections.emptyList(), roaPrefixes); } Map toRoaSpecifications(IncomingResourceCertificate currentIncomingCertificate) { @@ -114,14 +110,66 @@ Map toRoaSpecifications(IncomingResourceCertificate curre return result; } - private static Set convertToSet(Map byPrefix) { - return byPrefix.entrySet().stream().map(prefix -> new RoaConfigurationPrefix(prefix.getKey(), prefix.getValue())).collect(Collectors.toSet()); + private static Pair prefixKey(RoaConfigurationPrefix r) { + return Pair.of(r.getAsn(), r.getPrefix()); + } + + private static Triple prefixMinimalIdentity(RoaConfigurationPrefix r) { + return Triple.of(r.getAsn(), r.getPrefix(), r.getMaximumLength()); + } + + private static boolean differentPrefixes(RoaConfigurationPrefix r1, RoaConfigurationPrefix r2) { + return r1 == null ? r2 != null : r2 == null || !prefixMinimalIdentity(r1).equals(prefixMinimalIdentity(r2)); } - private static Map convertToMap(Collection prefixes) { - return prefixes.stream().collect(Collectors.toMap( - prefix -> new AnnouncedRoute(prefix.getAsn(), prefix.getPrefix()), - RoaConfigurationPrefix::getMaximumLength, - (a, b) -> b)); + /** + * Sort the ROA prefixes and keep the first unique one by earliest updatedAt + */ + private Set canonicalRoaPrefixes(Stream input) { + var prefixList = input.sorted(ROA_CONFIGURATION_PREFIX_COMPARATOR) + .filter(Streams.distinctByKey(RoaConfiguration::prefixKey)) + .toList(); + + return new HashSet<>(prefixList); } + + /** + * Apply added and deleted prefixes, and return the ones that were really + * added or really deleted based on the current set of prefixes and certain + * heuristics such as max_length and update_at fields. + */ + public PrefixDiff mergePrefixes( + Collection prefixesToAdd, + Collection prefixesToRemove) { + + var toRemove = prefixesToRemove.stream() + .map(RoaConfiguration::prefixMinimalIdentity) + .collect(Collectors.toSet()); + + var newPrefixes = Stream.concat(prefixes.stream(), prefixesToAdd.stream()) + .filter(r -> !toRemove.contains(prefixMinimalIdentity(r))) + .collect(Collectors.toMap( + RoaConfiguration::prefixKey, + Function.identity(), + // Prefer prefix based on the maxLength + updatedAt heuristics + // defined by the comparator + (r1, r2) -> ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(r1, r2) < 0 ? r1 : r2)); + + var existing = prefixes.stream() + .collect(Collectors.toMap(RoaConfiguration::prefixKey, Function.identity())); + + var removed = prefixes.stream() + .filter(r -> differentPrefixes(r, newPrefixes.get(prefixKey(r)))) + .toList(); + + var added = newPrefixes.values().stream() + .filter(r -> differentPrefixes(r, existing.get(prefixKey(r)))) + .toList(); + + added.forEach(r -> r.setRoaConfiguration(this)); + prefixes = new HashSet<>(newPrefixes.values()); + return new PrefixDiff(added, removed); + } + + public record PrefixDiff(List added, List removed) {} } diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceBean.java b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceBean.java index 9295255..7c4950f 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceBean.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceBean.java @@ -65,12 +65,9 @@ private void updateRoaConfigurationsForResources(ManagedCertificateAuthority ca, .collect(Collectors.toSet()); if (!toBeRemoved.isEmpty()) { - // Update the config, log the removed prefixes [...] - config.removePrefix(toBeRemoved); + roaConfigurationRepository.removePrefixes(config, toBeRemoved); ca.markConfigurationUpdated(); - roaConfigurationRepository.logRoaPrefixDeletion(config, toBeRemoved); - context.recordEvent( new RoaConfigurationUpdatedDueToChangedResourcesEvent( ca.getVersionedId(), diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationPrefix.java b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationPrefix.java index 1cce290..c9e3678 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationPrefix.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationPrefix.java @@ -1,49 +1,81 @@ package net.ripe.rpki.domain.roa; -import lombok.Getter; +import jakarta.persistence.*; +import lombok.*; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.ipresource.IpResourceType; import net.ripe.rpki.commons.validation.roa.AnnouncedRoute; +import net.ripe.rpki.ripencc.support.persistence.AsnPersistenceConverter; import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; +import java.io.Serializable; import java.math.BigInteger; import java.time.Instant; import java.util.List; -import static com.google.common.base.Objects.*; -@Embeddable +@EqualsAndHashCode +@Entity +@Table(name = "roaconfiguration_prefixes") +@IdClass(RoaConfigurationPrefix.RoaConfigurationPrefixIdClass.class) public class RoaConfigurationPrefix { + + @NoArgsConstructor + @Data + public static class RoaConfigurationPrefixIdClass implements Serializable { + // via https://stackoverflow.com/a/61258208 + @Column(name = "asn", nullable = false) + @Convert(converter = AsnPersistenceConverter.class) + private Asn asn; + private BigInteger prefixStart; + private BigInteger prefixEnd; + @Column(name = "prefix_type_id", nullable = false) + private IpResourceType prefixType; + } + + @Id @Column(name = "asn", nullable = false) @Getter private Asn asn; + @Id @Column(name = "prefix_start", nullable = false) private BigInteger prefixStart; + @Id @Column(name = "prefix_end", nullable = false) private BigInteger prefixEnd; + @Id @Column(name = "prefix_type_id", nullable = false) private IpResourceType prefixType; - // Nullable for database compatibility reasons. - @Column(name = "maximum_length", nullable = true) + @Column(name = "maximum_length") private Integer maximumLength; + @ManyToOne + @JoinColumn(name = "roaconfiguration_id", nullable = false) + @Setter + private RoaConfiguration roaConfiguration; + @Getter - @Column(name = "updated_at", insertable = false, updatable = false) + @Setter + @Column(name = "updated_at") private Instant updatedAt; protected RoaConfigurationPrefix() { // JPA uses this } + @PreUpdate + @PrePersist + void prePersist() { + this.updatedAt = Instant.now(); + } + public RoaConfigurationPrefix(Asn asn, IpRange prefix) { this(asn, prefix, null); } @@ -52,12 +84,17 @@ public RoaConfigurationPrefix(Asn asn, IpRange prefix, Integer maximumLength) { this(new RoaConfigurationPrefixData(asn, prefix, maximumLength)); } + public RoaConfigurationPrefix(Asn asn, IpRange prefix, Integer maximumLength, Instant updatedAt) { + this(new RoaConfigurationPrefixData(asn, prefix, maximumLength, updatedAt)); + } + public RoaConfigurationPrefix(RoaConfigurationPrefixData data) { this.asn = data.getAsn(); this.prefixType = data.getPrefix().getType(); this.prefixStart = data.getPrefix().getStart().getValue(); this.prefixEnd = data.getPrefix().getEnd().getValue(); this.maximumLength = data.getMaximumLength(); + this.updatedAt = data.getUpdatedAt(); } public RoaConfigurationPrefix(AnnouncedRoute route, Integer maximumLength) { @@ -76,36 +113,13 @@ public RoaConfigurationPrefixData toData() { return new RoaConfigurationPrefixData(getAsn(), getPrefix(), getMaximumLength(), getUpdatedAt()); } - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + getAsn().hashCode(); - result = prime * result + getPrefix().hashCode(); - result = prime * result + getMaximumLength(); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - RoaConfigurationPrefix that = (RoaConfigurationPrefix) obj; - return equal(getAsn(), that.getAsn()) - && equal(this.getPrefix(), that.getPrefix()) - && this.getMaximumLength() == that.getMaximumLength(); - } - @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) .append("asn", getAsn()) .append("prefix", getPrefix()) .append("maximumLength", getMaximumLength()) + .append("updatedAt", getUpdatedAt()) .toString(); } diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java index 2fd2d19..e5c1a76 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java @@ -3,10 +3,12 @@ import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpResourceRange; import net.ripe.rpki.domain.ManagedCertificateAuthority; +import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; import net.ripe.rpki.server.api.support.objects.CaName; import java.time.Instant; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -22,9 +24,7 @@ public interface RoaConfigurationRepository { Collection findAll(); - List findAllPerCa(); - - void logRoaPrefixDeletion(RoaConfiguration configuration, Collection deletedPrefixes); + Collection findAllPrefixes(); int countRoaPrefixes(); @@ -32,18 +32,15 @@ public interface RoaConfigurationRepository { void remove(RoaConfiguration roaConfiguration); - class RoaConfigurationPerCa { - public final Long caId; - public final CaName caName; - public final Asn asn; - public final IpResourceRange prefix; - public final Integer maximumLength; - public RoaConfigurationPerCa(Long caId, CaName caName, Asn asn, IpResourceRange prefix, Integer maximumLength) { - this.caId = caId; - this.caName = caName; - this.asn = asn; - this.prefix = prefix; - this.maximumLength = maximumLength; - } + default void addPrefixes(RoaConfiguration roaConfiguration, Collection prefixes) { + mergePrefixes(roaConfiguration, prefixes, Collections.emptyList()); + } + + default void removePrefixes(RoaConfiguration roaConfiguration, Collection prefixes) { + mergePrefixes(roaConfiguration, Collections.emptyList(), prefixes); } + + void mergePrefixes(RoaConfiguration configuration, + Collection prefixesToAdd, + Collection prefixesToRemove); } diff --git a/src/main/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesService.java b/src/main/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesService.java index 8ff32c0..af77fbd 100644 --- a/src/main/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesService.java +++ b/src/main/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesService.java @@ -19,6 +19,7 @@ import org.springframework.web.context.request.WebRequest; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -49,12 +50,10 @@ public ResponseEntity> list return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } - List roas = roaConfigurationRepository.findAll() - .stream() - .flatMap(rc -> rc.getPrefixes().stream()) - .map(RoaConfigurationPrefix::toData) + List roas = roaConfigurationRepository.findAllPrefixes().stream() .sorted(RoaPrefixData.ROA_PREFIX_DATA_COMPARATOR) .toList(); + return ResponseEntity.ok(ValidatedObjectsResponse.of(roas, Collections.singletonMap("origin", "rpki-core"))); } diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java index b07a6d4..aa86504 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java @@ -22,7 +22,10 @@ public class UpdateRoaConfigurationCommand extends CertificateAuthorityModificat private final List deletions; - public UpdateRoaConfigurationCommand(VersionedId certificateAuthorityId, Optional ifMatch, Collection added, Collection deleted) { + public UpdateRoaConfigurationCommand(VersionedId certificateAuthorityId, + Optional ifMatch, + Collection added, + Collection deleted) { super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); this.ifMatch = ifMatch; this.additions = new ArrayList<>(added); diff --git a/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java b/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java index 924f5db..448cf7b 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java @@ -13,7 +13,6 @@ import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; import java.time.Instant; -import java.util.Comparator; import static com.google.common.base.Objects.*; import static com.google.common.base.Preconditions.*; @@ -51,6 +50,7 @@ public class RoaConfigurationPrefixData extends ValueObjectSupport implements Ro * No object can have a updatedAt before the point in time this feature was added. */ @Getter + @JsonProperty(access = JsonProperty.Access.READ_ONLY) @JsonInclude(JsonInclude.Include.NON_NULL) private final Instant updatedAt; diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java index dff6fc7..43986fb 100644 --- a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java @@ -21,10 +21,7 @@ import jakarta.inject.Inject; import java.util.Collection; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; @Handler @@ -47,7 +44,6 @@ public UpdateRoaConfigurationCommandHandler(CertificateAuthorityRepository certi Preconditions.checkArgument(privateAsnRanges.stream().allMatch(a -> a.getType() == IpResourceType.ASN), "Only ASNs allowed for private ASN ranges: %s", privateAsnRanges); } - @Override public Class commandType() { return UpdateRoaConfigurationCommand.class; @@ -62,7 +58,9 @@ public void handle(@NonNull UpdateRoaConfigurationCommand command, CommandStatus validateAsns(command); validateAddedPrefixes(ca, command.getAdditions()); - applyUpdates(command, configuration); + roaConfigurationRepository.mergePrefixes(configuration, + RoaConfigurationPrefix.fromData(command.getAdditions()), + RoaConfigurationPrefix.fromData(command.getDeletions())); ca.markConfigurationUpdated(); @@ -70,22 +68,6 @@ public void handle(@NonNull UpdateRoaConfigurationCommand command, CommandStatus roaMetricsService.countDeleted(command.getDeletions().size()); } - private void applyUpdates(UpdateRoaConfigurationCommand command, RoaConfiguration configuration) { - final Set formerPrefixes = new HashSet<>(configuration.getPrefixes()); - - Collection addedPrefixes = RoaConfigurationPrefix.fromData(command.getAdditions()); - Collection deletedPrefixes = RoaConfigurationPrefix.fromData(command.getDeletions()); - - configuration.addPrefix(addedPrefixes); - configuration.removePrefix(deletedPrefixes); - - if (!deletedPrefixes.isEmpty()) { - final Set actualDeletable = - deletedPrefixes.stream().filter(formerPrefixes::contains).collect(Collectors.toSet()); - roaConfigurationRepository.logRoaPrefixDeletion(configuration, actualDeletable); - } - } - private void validateEntityTag(UpdateRoaConfigurationCommand command, RoaConfiguration configuration) { command.getIfMatch().ifPresent(ifMatch -> { String entityTag = configuration.convertToData().entityTag(); diff --git a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java index c9f4c2c..0680c67 100644 --- a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java @@ -1,26 +1,22 @@ package net.ripe.rpki.services.impl.jpa; -import net.ripe.ipresource.Asn; -import net.ripe.ipresource.IpRange; -import net.ripe.ipresource.IpResourceRange; -import net.ripe.ipresource.IpResourceType; +import jakarta.persistence.Query; +import net.ripe.ipresource.*; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.domain.roa.RoaConfiguration; import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; import net.ripe.rpki.domain.roa.RoaConfigurationRepository; import net.ripe.rpki.ripencc.support.persistence.JpaRepository; -import net.ripe.rpki.server.api.support.objects.CaName; +import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import jakarta.persistence.NoResultException; -import javax.security.auth.x500.X500Principal; + import java.math.BigDecimal; import java.math.BigInteger; -import java.sql.Timestamp; import java.time.Instant; import java.util.Collection; -import java.util.List; import java.util.Optional; @Repository @@ -49,56 +45,26 @@ public RoaConfiguration getOrCreateByCertificateAuthority(ManagedCertificateAuth @Override @SuppressWarnings("unchecked") - public List findAllPerCa() { - return (List) createNativeQuery("SELECT DISTINCT\n" + - " ca.id,\n" + - " ca.name,\n" + - " rcp.asn,\n" + - " rcp.prefix_type_id,\n" + - " rcp.prefix_start,\n" + - " rcp.prefix_end,\n" + - " rcp.maximum_length\n" + - "FROM certificateauthority ca\n" + - "JOIN roaconfiguration rc ON rc.certificateauthority_id = ca.id\n" + - "JOIN roaconfiguration_prefixes rcp ON rcp.roaconfiguration_id = rc.id") + public Collection findAllPrefixes() { + return createNativeQuery( + "SELECT DISTINCT asn, prefix_type_id, prefix_start, prefix_end, maximum_length FROM roaconfiguration_prefixes") .getResultList() .stream() .map(o -> { final Object[] row = (Object[]) o; - final Long caId = ((Long) row[0]); - final X500Principal principal = new X500Principal((String) row[1]); - final CaName caName = CaName.of(principal); - final Asn asn = new Asn(((BigDecimal) row[2]).longValue()); - final Short prefixType = (Short) row[3]; - final BigInteger begin = ((BigDecimal) row[4]).toBigInteger(); - final BigInteger end = ((BigDecimal) row[5]).toBigInteger(); - final Integer maximumLength = (Integer) row[6]; + final Asn asn = new Asn(((BigDecimal) row[0]).longValue()); + final Short prefixType = (Short) row[1]; + final BigInteger begin = ((BigDecimal) row[2]).toBigInteger(); + final BigInteger end = ((BigDecimal) row[3]).toBigInteger(); + final Integer maximumLength = (Integer) row[4]; final IpResourceType resourceType = IpResourceType.values()[prefixType]; - final IpResourceRange range = resourceType.fromBigInteger(begin).upTo(resourceType.fromBigInteger(end)); - return new RoaConfigurationPerCa(caId, caName, asn, range, maximumLength); + final IpRange range = IpRange.range( + (IpAddress)resourceType.fromBigInteger(begin), + (IpAddress)resourceType.fromBigInteger(end)); + return new RoaConfigurationPrefixData(asn, range, maximumLength); }).toList(); } - @Override - public void logRoaPrefixDeletion(RoaConfiguration configuration, Collection deletedPrefixes) { - // do it in SQL because Hibernate makes it harder to have the same enity - String sql = "INSERT INTO deleted_roaconfiguration_prefixes " + - " (roaconfiguration_id, asn, prefix_type_id, prefix_start, prefix_end, maximum_length)" + - " VALUES (:roaconfiguration_id, :asn, :prefix_type_id, :prefix_start, :prefix_end, :maximum_length)"; - - deletedPrefixes.forEach(dp -> { - final IpRange prefix = dp.getPrefix(); - createNativeQuery(sql) - .setParameter("roaconfiguration_id", configuration.getId()) - .setParameter("asn", dp.getAsn().longValue()) - .setParameter("prefix_type_id", prefix.getType() == IpResourceType.IPv4 ? 1 : 2) - .setParameter("prefix_start", prefix.getStart().getValue()) - .setParameter("prefix_end", prefix.getEnd().getValue()) - .setParameter("maximum_length", dp.getMaximumLength()) - .executeUpdate(); - }); - } - @Override public int countRoaPrefixes() { String sql = "SELECT count(*) from roaconfiguration_prefixes"; @@ -117,6 +83,66 @@ public Optional lastModified() { return Optional.ofNullable(res); } + @Override + public void mergePrefixes(RoaConfiguration configuration, + Collection prefixesToAdd, + Collection prefixesToRemove) { + var diff = configuration.mergePrefixes(prefixesToAdd, prefixesToRemove); + applyDiff(configuration, diff); + } + + public void applyDiff(RoaConfiguration configuration, + RoaConfiguration.PrefixDiff diff) { + + diff.removed().forEach(r -> { + String sql = """ + WITH deleted AS ( + DELETE FROM roaconfiguration_prefixes + WHERE roaconfiguration_id = :roaconfiguration_id + AND asn = :asn + AND prefix_type_id = :prefix_type_id + AND prefix_start = :prefix_start + AND prefix_end = :prefix_end + AND maximum_length = :maximum_length + RETURNING * + ) + INSERT INTO deleted_roaconfiguration_prefixes + SELECT * FROM deleted + """; + executeForPrefix(configuration, r, sql); + }); + + diff.added().forEach(r -> { + String sql = """ + INSERT INTO roaconfiguration_prefixes (roaconfiguration_id, asn, prefix_type_id, prefix_start, prefix_end, maximum_length) + VALUES (:roaconfiguration_id, :asn, :prefix_type_id, :prefix_start, :prefix_end, :maximum_length) + RETURNING updated_at + """; + Instant updatedAt = extractForPrefix(configuration, r, sql); + r.setUpdatedAt(updatedAt); + }); + } + + private void executeForPrefix(RoaConfiguration configuration, RoaConfigurationPrefix dp, String sql) { + final IpRange prefix = dp.getPrefix(); + makeQuery(configuration, dp, sql, prefix).executeUpdate(); + } + + private Instant extractForPrefix(RoaConfiguration configuration, RoaConfigurationPrefix dp, String sql) { + final IpRange prefix = dp.getPrefix(); + return (Instant) makeQuery(configuration, dp, sql, prefix).getSingleResult(); + } + + private Query makeQuery(RoaConfiguration configuration, RoaConfigurationPrefix dp, String sql, IpRange prefix) { + return createNativeQuery(sql) + .setParameter("roaconfiguration_id", configuration.getId()) + .setParameter("asn", dp.getAsn().longValue()) + .setParameter("prefix_type_id", prefix.getType() == IpResourceType.IPv4 ? 1 : 2) + .setParameter("prefix_start", prefix.getStart().getValue()) + .setParameter("prefix_end", prefix.getEnd().getValue()) + .setParameter("maximum_length", dp.getMaximumLength()); + } + @Override protected Class getEntityClass() { return RoaConfiguration.class; diff --git a/src/main/java/net/ripe/rpki/util/Streams.java b/src/main/java/net/ripe/rpki/util/Streams.java index 694256b..5cc6dbd 100644 --- a/src/main/java/net/ripe/rpki/util/Streams.java +++ b/src/main/java/net/ripe/rpki/util/Streams.java @@ -4,9 +4,11 @@ import java.security.MessageDigest; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BinaryOperator; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collector; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,6 +27,13 @@ public static Collection> grouped(final List s, final int chunkSi return grouped(s.stream(), chunkSize); } + public static Predicate distinctByKey( + Function keyExtractor) { + + var seen = new ConcurrentHashMap<>(); + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } + @SneakyThrows public static String entityTag(Stream data) { MessageDigest digest = MessageDigest.getInstance("SHA-256"); diff --git a/src/main/resources/db/migration/V132__roaconfiguration_prefixes_add_id.sql b/src/main/resources/db/migration/V132__roaconfiguration_prefixes_add_id.sql new file mode 100644 index 0000000..a7dcc26 --- /dev/null +++ b/src/main/resources/db/migration/V132__roaconfiguration_prefixes_add_id.sql @@ -0,0 +1,40 @@ +UPDATE roaconfiguration_prefixes +SET maximum_length = 32 - sb.effective_length FROM (SELECT (log(prefix_end - prefix_start + 1) / log(2)) as effective_length, + asn, + prefix_type_id, + prefix_start, + prefix_end + FROM roaconfiguration_prefixes + WHERE + maximum_length IS NULL + AND + prefix_type_id = 1 + ) as sb +WHERE roaconfiguration_prefixes.maximum_length IS NULL + AND roaconfiguration_prefixes.asn = sb.asn + AND roaconfiguration_prefixes.prefix_type_id = sb.prefix_type_id + AND roaconfiguration_prefixes.prefix_start = sb.prefix_start + AND roaconfiguration_prefixes.prefix_end = sb.prefix_end; + +UPDATE roaconfiguration_prefixes +SET maximum_length = 128 - sb.effective_length FROM (SELECT (log(prefix_end - prefix_start + 1) / log(2)) as effective_length, + asn, + prefix_type_id, + prefix_start, + prefix_end + FROM roaconfiguration_prefixes + WHERE + maximum_length IS NULL + AND + prefix_type_id = 2 + ) as sb +WHERE roaconfiguration_prefixes.maximum_length IS NULL + AND roaconfiguration_prefixes.asn = sb.asn + AND roaconfiguration_prefixes.prefix_type_id = sb.prefix_type_id + AND roaconfiguration_prefixes.prefix_start = sb.prefix_start + AND roaconfiguration_prefixes.prefix_end = sb.prefix_end; + +ALTER TABLE roaconfiguration_prefixes ALTER COLUMN maximum_length SET NOT NULL; + +CREATE INDEX roaconfiguration_prefixes_roaconfiguration_id ON roaconfiguration_prefixes(roaconfiguration_id); + diff --git a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java index ce004d9..741a998 100644 --- a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java +++ b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java @@ -24,6 +24,7 @@ import javax.security.auth.x500.X500Principal; import java.security.SecureRandom; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -75,14 +76,14 @@ public void setUp() { // Add the ROA configuration var ca = certificateAuthorityRepository.findManagedCa(HOSTED_CA_ID); var roaConfiguration = roaConfigurationRepository.getOrCreateByCertificateAuthority(ca); - roaConfiguration.addPrefix(ALL_ROA_CONFIGURATIONS); + roaConfigurationRepository.addPrefixes(roaConfiguration, ALL_ROA_CONFIGURATIONS); resourceCache.updateEntry(CaName.of(CHILD_CA_NAME), parse("fc00::/8")); execute(new UpdateAllIncomingResourceCertificatesCommand(new VersionedId(HOSTED_CA_ID, VersionedId.INITIAL_VERSION), Integer.MAX_VALUE)); } private Pair createAnotherCa(int roaCount) { - var randomId = HOSTED_CA_ID + new SecureRandom().nextLong(1<<60); + var randomId = HOSTED_CA_ID + new SecureRandom().nextLong(1L << 60); // Add another CA with ROAs final var secondChildCaName = new X500Principal("CN=" + randomId); @@ -97,7 +98,7 @@ private Pair createAnotherCa(int roaCount) { .mapToObj(i -> new RoaConfigurationPrefix(Asn.parse(Integer.toString(65443 + i)), IpRange.parse("192.0.2.0/24")) ).collect(Collectors.toList()); - roaConfiguration.addPrefix(randomRoas); + roaConfigurationRepository.addPrefixes(roaConfiguration, randomRoas); resourceCache.updateEntry(CaName.of(secondChildCaName), parse("192.0.2.0/24")); execute(new UpdateAllIncomingResourceCertificatesCommand(new VersionedId(randomId, VersionedId.INITIAL_VERSION), Integer.MAX_VALUE)); @@ -112,7 +113,7 @@ public void testGetCaStat() { .allMatch(ca -> ca.caName.equals(CHILD_CA_NAME.getName())); var second = createAnotherCa(42); - // Should have two CAs, one of which as 42 ROAs + // Should have two CAs, one of which has 42 ROAs assertThat(subject.getCaStats()) .hasSize(2) .anyMatch(thatCa -> second.getKey().getName().equals(thatCa.caName) && thatCa.roas == 42); diff --git a/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java b/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java index 0867df8..674887a 100644 --- a/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java +++ b/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java @@ -107,7 +107,7 @@ public void setupTest() { protected void clearDatabase() { // Clean the test database. Note that this is not transactional, but the test database should be empty anyway. - entityManager.createNativeQuery("TRUNCATE TABLE certificateauthority, commandaudit, ta_published_object, resource_cache CASCADE").executeUpdate(); + entityManager.createNativeQuery("TRUNCATE TABLE certificateauthority, commandaudit, ta_published_object, resource_cache, roaconfiguration CASCADE").executeUpdate(); resourceCache.populateCache(Map.of(CaName.of(repositoryConfiguration.getProductionCaPrincipal()), ImmutableResourceSet.ALL_PRIVATE_USE_RESOURCES)); } diff --git a/src/test/java/net/ripe/rpki/domain/manifest/ManifestPublicationServiceTest.java b/src/test/java/net/ripe/rpki/domain/manifest/ManifestPublicationServiceTest.java index 0b3547f..96f645c 100644 --- a/src/test/java/net/ripe/rpki/domain/manifest/ManifestPublicationServiceTest.java +++ b/src/test/java/net/ripe/rpki/domain/manifest/ManifestPublicationServiceTest.java @@ -148,7 +148,9 @@ public void should_update_both_manifest_and_crl_when_manifest_needs_update() { publishedObjectRepository.findAll().forEach(po -> po.published()); entityManager.flush(); - roaConfigurationRepository.getOrCreateByCertificateAuthority(ca).addPrefix(Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10.0.0.0/8")))); + var configuration = roaConfigurationRepository.getOrCreateByCertificateAuthority(ca); + roaConfigurationRepository.addPrefixes(configuration, + Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10.0.0.0/8")))); roaEntityService.updateRoasIfNeeded(ca); subject.updateManifestAndCrlIfNeeded(ca.getCurrentKeyPair()); diff --git a/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceTest.java b/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceTest.java index 4807542..4c1a93a 100644 --- a/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceTest.java +++ b/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationMaintenanceServiceTest.java @@ -14,16 +14,15 @@ import net.ripe.rpki.server.api.services.command.CommandService; import net.ripe.rpki.server.api.services.command.CommandStatus; import net.ripe.rpki.server.api.support.objects.CaName; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import javax.security.auth.x500.X500Principal; import jakarta.transaction.Transactional; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.UUID; + +import java.util.*; import static net.ripe.ipresource.ImmutableResourceSet.parse; import static org.assertj.core.api.Assertions.assertThat; @@ -69,9 +68,8 @@ public void setUp() { certificateAuthorityRepository.add(child); // Add the ROA configuration - var ca = certificateAuthorityRepository.findManagedCa(HOSTED_CA_ID); - var roaConfiguration = roaConfigurationRepository.getOrCreateByCertificateAuthority(ca); - roaConfiguration.addPrefix(ALL_ROA_CONFIGURATIONS); + var roaConfiguration = roaConfigurationRepository.getOrCreateByCertificateAuthority(child); + roaConfigurationRepository.addPrefixes(roaConfiguration, ALL_ROA_CONFIGURATIONS); resourceCache.updateEntry(CaName.of(CHILD_CA_NAME), parse("fc00::/8")); execute(new UpdateAllIncomingResourceCertificatesCommand(new VersionedId(HOSTED_CA_ID, VersionedId.INITIAL_VERSION), Integer.MAX_VALUE)); diff --git a/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationTest.java b/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationTest.java index d31d795..09013eb 100644 --- a/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationTest.java +++ b/src/test/java/net/ripe/rpki/domain/roa/RoaConfigurationTest.java @@ -3,49 +3,135 @@ import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.rpki.domain.ManagedCertificateAuthority; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.Collections; +import java.util.List; -import static org.junit.Assert.*; +import static net.ripe.rpki.domain.roa.RoaConfiguration.ROA_CONFIGURATION_PREFIX_COMPARATOR; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; -public class RoaConfigurationTest { +class RoaConfigurationTest { + static final Asn DOCUMENTATION_AS_1 = Asn.parse("AS64396"); + static final Asn DOCUMENTATION_AS_2 = Asn.parse("AS64397"); + static final IpRange TEST_NET_1 = IpRange.parse("192.0.2.0/24"); + static final IpRange TEST_NET_2 = IpRange.parse("198.51.100.0/24"); - private static final RoaConfigurationPrefix AS3333_10_8_NULL = new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10/8"), null); - private static final RoaConfigurationPrefix AS3333_10_8_16 = new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10/8"), 16); + // We re-use the same instant for multiple objects + private final static Instant THEN = Instant.now(); + + // recall: The effective maximum length is the prefix length by default - specifying null is identical to a value + private static final RoaConfigurationPrefix AS64396_TEST_NET_1_24 = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_1, null, THEN); + private static final RoaConfigurationPrefix AS64396_TEST_NET_1_32 = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_1, 32, THEN); + private static final RoaConfigurationPrefix AS64396_TEST_NET_2_24 = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_2, null, THEN); + + private static final RoaConfigurationPrefix AS64397_TEST_NET_1_24 = new RoaConfigurationPrefix(DOCUMENTATION_AS_2, TEST_NET_1, null, THEN); private ManagedCertificateAuthority certificateAuthority; private RoaConfiguration subject; - @Before - public void setUp() { + @BeforeEach + void setUp() { certificateAuthority = mock(ManagedCertificateAuthority.class); subject = new RoaConfiguration(certificateAuthority, Collections.emptyList()); } @Test - public void should_add_roa_prefix() { - subject.addPrefix(Collections.singleton(AS3333_10_8_NULL)); + void should_add_roa_prefix() { + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_24); + + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_2_24)); + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_24, AS64396_TEST_NET_2_24); + } + + @Test + void should_add_multiple() { + subject.addPrefixes(List.of(AS64396_TEST_NET_1_32, AS64396_TEST_NET_2_24)); + assertThat(subject.getPrefixes()).containsExactlyInAnyOrder(AS64396_TEST_NET_1_32, AS64396_TEST_NET_2_24); + } + + @Test + void should_replace_roa_prefix_when_only_maximum_length_changed() { + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_32)); + + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_32); + } + + @Test + void should_retain_most_specific_on_conflict() { + // later wins + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_32)); - assertEquals(Collections.singleton(AS3333_10_8_NULL), subject.getPrefixes()); + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_32); + // more specific does not get overwritten + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_32); + } + + @Test + void should_retain_first_on_conflict() { + var laterConflict = new RoaConfigurationPrefix(AS64396_TEST_NET_1_32.getAsn(), AS64396_TEST_NET_1_32.getPrefix(), AS64396_TEST_NET_1_32.getMaximumLength(), Instant.now().plusSeconds(3600)); + + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_32)); + subject.addPrefixes(Collections.singleton(laterConflict)); + + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_32); + } + + @Test + void should_remove_roa_prefix() { + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + assertThat(subject.getPrefixes()).isNotEmpty(); + subject.removePrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + + assertThat(subject.getPrefixes()).isEmpty(); } @Test - public void should_replace_roa_prefix_when_only_maximum_length_changed() { - subject.addPrefix(Collections.singleton(AS3333_10_8_NULL)); - subject.addPrefix(Collections.singleton(AS3333_10_8_16)); + void should_replace_with_more_specific() { + subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_24)); + var diff = subject.mergePrefixes(Collections.singleton(AS64396_TEST_NET_1_32), Collections.singleton(AS64396_TEST_NET_1_24)); + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_32); + assertThat(diff.removed()).containsOnly(AS64396_TEST_NET_1_24); + assertThat(diff.added()).containsOnly(AS64396_TEST_NET_1_32); + } - assertEquals(Collections.singleton(AS3333_10_8_16), subject.getPrefixes()); + @Test + void should_replace_with_less_specific() { + var diff1 = subject.addPrefixes(Collections.singleton(AS64396_TEST_NET_1_32)); + assertThat(diff1.added()).containsOnly(AS64396_TEST_NET_1_32); + assertThat(diff1.removed()).isEmpty(); + var diff = subject.mergePrefixes(Collections.singleton(AS64396_TEST_NET_1_24), Collections.singleton(AS64396_TEST_NET_1_32)); + assertThat(subject.getPrefixes()).containsOnly(AS64396_TEST_NET_1_24); + assertThat(diff.removed()).containsOnly(AS64396_TEST_NET_1_32); + assertThat(diff.added()).containsOnly(AS64396_TEST_NET_1_24); } @Test - public void should_remove_roa_prefix() { - subject.addPrefix(Collections.singleton(AS3333_10_8_NULL)); + void comparator_should_sort() { + // Get two equal objects, even equal time. + var as64396TestNet1_24_Explicit = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_1, 24, THEN); + assertThat(ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(as64396TestNet1_24_Explicit, AS64396_TEST_NET_1_24)).isZero(); + + // The new object is before the other -> should be before + var as64396TestNet1_24_before = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_1, null, THEN.minusSeconds(3600)); + assertThat(ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(AS64396_TEST_NET_1_24, as64396TestNet1_24_before)).isPositive(); + + // The new object has null time -> should be after + var as64396TestNet1_24_null = new RoaConfigurationPrefix(DOCUMENTATION_AS_1, TEST_NET_1, null, null); + assertThat(ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(AS64396_TEST_NET_1_24, as64396TestNet1_24_null)).isNegative(); - subject.removePrefix(Collections.singleton(AS3333_10_8_NULL)); + // Regular cases: + assertThat(ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(AS64396_TEST_NET_1_24, AS64396_TEST_NET_1_32)).isPositive(); + assertThat(ROA_CONFIGURATION_PREFIX_COMPARATOR.compare(AS64396_TEST_NET_1_24, AS64397_TEST_NET_1_24)).isNegative(); - assertEquals(Collections.EMPTY_SET, subject.getPrefixes()); } } diff --git a/src/test/java/net/ripe/rpki/domain/roa/RoaEntityServiceBeanTest.java b/src/test/java/net/ripe/rpki/domain/roa/RoaEntityServiceBeanTest.java index 8a8f0d8..9465a12 100644 --- a/src/test/java/net/ripe/rpki/domain/roa/RoaEntityServiceBeanTest.java +++ b/src/test/java/net/ripe/rpki/domain/roa/RoaEntityServiceBeanTest.java @@ -113,7 +113,7 @@ public void should_not_create_expired_roa_entity() { public void should_create_new_roa_entity_from_updated_specification() { RoaEntity roaEntity = handleRoaSpecificationCreatedEvent().getAddedRoa(); - configuration.addPrefix(Collections.singleton(new RoaConfigurationPrefix(ASN, IpRange.parse("192.168.0.0/26")))); + configuration.addPrefixes(Collections.singleton(new RoaConfigurationPrefix(ASN, IpRange.parse("192.168.0.0/26")))); RoaEntity newRoaEntity = handleRoaSpecificationUpdatedEvent(roaEntity).getAddedRoa(); @@ -125,7 +125,7 @@ public void should_create_new_roa_entity_from_updated_specification() { public void should_revoke_invalidated_roa_on_specification_update() { RoaEntity oldRoa = handleRoaSpecificationCreatedEvent().getAddedRoa(); - configuration.addPrefix(Collections.singleton(new RoaConfigurationPrefix(ASN, IpRange.parse("192.168.0.0/26")))); + configuration.addPrefixes(Collections.singleton(new RoaConfigurationPrefix(ASN, IpRange.parse("192.168.0.0/26")))); RoaEntity removedRoa = handleRoaSpecificationUpdatedEvent(oldRoa).getRemovedRoa(); @@ -210,7 +210,7 @@ public void should_skip_prefix_in_roa_configuration_if_not_covered_by_ca_resourc RoaEntity roaEntity = handleRoaSpecificationCreatedEvent().getAddedRoa(); assertNotNull(roaEntity); - configuration.addPrefix(Collections.singletonList(new RoaConfigurationPrefix(ASN, notCoveredPrefix, 24))); + configuration.addPrefixes(Collections.singletonList(new RoaConfigurationPrefix(ASN, notCoveredPrefix, 24))); RoaEntity newRoaEntity = handleRoaSpecificationUpdatedEvent(roaEntity).getAddedRoa(); assertNull(newRoaEntity); diff --git a/src/test/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesServiceTest.java index b4ca370..665a9b4 100644 --- a/src/test/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/monitoring/RoaPrefixesServiceTest.java @@ -9,6 +9,7 @@ import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; import net.ripe.rpki.domain.roa.RoaConfigurationRepository; import net.ripe.rpki.rest.service.Rest; +import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -99,17 +100,13 @@ public void shouldAcceptLastModifiedAndReturnArrayOtherwise() throws Exception { public void shouldReturnObjectsAsJsonMatchingValidatedObjectsShape() throws Exception { when(roaConfigurationRepository.lastModified()).thenReturn(Optional.of(Instant.now())); - when(roaConfigurationRepository.findAll()).thenReturn(Arrays.asList( - new RoaConfiguration(mock(ManagedCertificateAuthority.class), Arrays.asList( - new RoaConfigurationPrefix(Asn.parse("AS64496"), IpRange.parse("192.0.2.0/25"), 32), - new RoaConfigurationPrefix(Asn.parse("AS65536"), IpRange.parse("192.0.2.128/25"), 32), - new RoaConfigurationPrefix(Asn.parse("AS64496"), IpRange.parse("192.0.2.128/25"), 25) - )), - new RoaConfiguration(mock(ManagedCertificateAuthority.class), Arrays.asList( - new RoaConfigurationPrefix(Asn.parse("AS65551"), IpRange.parse("2001:DB8::/32"), 33), - new RoaConfigurationPrefix(Asn.parse("AS65550"), IpRange.parse("2001:DB8:ABCD::/48"), 48) - )) - )); + when(roaConfigurationRepository.findAllPrefixes()).thenReturn(Arrays.asList( + new RoaConfigurationPrefixData(Asn.parse("AS64496"), IpRange.parse("192.0.2.0/25"), 32), + new RoaConfigurationPrefixData(Asn.parse("AS65536"), IpRange.parse("192.0.2.128/25"), 32), + new RoaConfigurationPrefixData(Asn.parse("AS64496"), IpRange.parse("192.0.2.128/25"), 25), + new RoaConfigurationPrefixData(Asn.parse("AS65551"), IpRange.parse("2001:DB8::/32"), 33), + new RoaConfigurationPrefixData(Asn.parse("AS65550"), IpRange.parse("2001:DB8:ABCD::/48"), 48) + )); // shape: // ... diff --git a/src/test/java/net/ripe/rpki/services/impl/RoaServiceBeanTest.java b/src/test/java/net/ripe/rpki/services/impl/RoaServiceBeanTest.java index cbbfa91..c60f974 100644 --- a/src/test/java/net/ripe/rpki/services/impl/RoaServiceBeanTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/RoaServiceBeanTest.java @@ -84,7 +84,7 @@ public void getRoaConfiguration_should_default_to_empty_configuration() { @Test public void getRoaConfiguration_should_return_ca_roa_configuration() { RoaConfiguration roaConfiguration = new RoaConfiguration(certificateAuthority); - roaConfiguration.addPrefix(Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("127.0.0.0/8")))); + roaConfiguration.addPrefixes(Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("127.0.0.0/8")))); when(caRepository.findManagedCa(TEST_CA_ID)).thenReturn(certificateAuthority); when(roaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(Optional.of(roaConfiguration)); diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java index 37f1261..7cfc79d 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java @@ -79,7 +79,7 @@ public void should_clear_configuration_check_needed_even_if_configuration_change @Test public void should_update_roa_entities() { - roaConfigurationRepository.getOrCreateByCertificateAuthority(ca).addPrefix(Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10.0.0.0/8")))); + roaConfigurationRepository.getOrCreateByCertificateAuthority(ca).addPrefixes(Collections.singleton(new RoaConfigurationPrefix(Asn.parse("AS3333"), IpRange.parse("10.0.0.0/8")))); ca.markConfigurationUpdated(); assertThat(roaEntityRepository.findCurrentByCertificateAuthority(ca)).describedAs("current ROA entities").isEmpty(); diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java index 1be7561..daf500b 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java @@ -1,10 +1,10 @@ package net.ripe.rpki.services.impl.handlers; +import jakarta.transaction.Transactional; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; -import net.ripe.rpki.domain.CertificateAuthorityRepository; +import net.ripe.rpki.domain.CertificationDomainTestCase; import net.ripe.rpki.domain.ManagedCertificateAuthority; -import net.ripe.rpki.domain.TestObjects; import net.ripe.rpki.domain.roa.RoaConfiguration; import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; import net.ripe.rpki.domain.roa.RoaConfigurationRepository; @@ -16,6 +16,7 @@ import net.ripe.rpki.services.impl.background.RoaMetricsService; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; import java.util.Optional; @@ -25,65 +26,66 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -public class UpdateRoaConfigurationCommandHandlerTest { +@Transactional +public class UpdateRoaConfigurationCommandHandlerTest extends CertificationDomainTestCase { private static final Asn ASN = Asn.parse("1234"); private static final String PRIVATE_ASNS = "64512-65535, 4200000000-4294967294"; private static final Asn PRIVATE_ASN = Asn.parse("AS64614"); - private static final IpRange PREFIX = IpRange.parse("10.1/16"); + private static final IpRange PREFIX1 = IpRange.parse("10.0.0.0/8"); + private static final IpRange PREFIX2 = IpRange.parse("172.16.0.0/12"); + private static final IpRange PREFIX3 = IpRange.parse("192.168.0.0/16"); private ManagedCertificateAuthority certificateAuthority; - private CertificateAuthorityRepository certificateAuthorityRepository; - + @Autowired private RoaConfigurationRepository roaConfigurationRepository; private UpdateRoaConfigurationCommandHandler subject; - private RoaConfiguration configuration; - private RoaMetricsService roaMetricsService; @Before public void setUp() { - certificateAuthority = TestObjects.createInitialisedProdCaWithRipeResources(); - certificateAuthorityRepository = mock(CertificateAuthorityRepository.class); - roaConfigurationRepository = mock(RoaConfigurationRepository.class); + clearDatabase(); + certificateAuthority = createInitialisedProdCaWithRipeResources(); roaMetricsService = mock(RoaMetricsService.class); - - when(certificateAuthorityRepository.findManagedCa(certificateAuthority.getId())).thenReturn(certificateAuthority); - - subject = new UpdateRoaConfigurationCommandHandler(certificateAuthorityRepository, roaConfigurationRepository - , PRIVATE_ASNS, roaMetricsService); - configuration = new RoaConfiguration(certificateAuthority); - - when(roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority)).thenReturn(configuration); + subject = new UpdateRoaConfigurationCommandHandler(certificateAuthorityRepository, + roaConfigurationRepository, PRIVATE_ASNS, roaMetricsService); } @Test public void should_add_new_additions() { + var configuration = new RoaConfiguration(certificateAuthority); subject.handle(new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.of(configuration.convertToData().entityTag()), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX, null)), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), Collections.emptyList())); - assertThat(configuration.getPrefixes()).isEqualTo(Collections.singleton(new RoaConfigurationPrefix(ASN, PREFIX, null))); + var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); + assertThat(config.getPrefixes()).hasSize(1); + RoaConfigurationPrefix p = config.getPrefixes().iterator().next(); + assertThat(p.getAsn()).isEqualTo(ASN); + assertThat(p.getPrefix()).isEqualTo(PREFIX1); + assertThat(p.getMaximumLength()).isEqualTo(8); + assertThat(p.getUpdatedAt()).isNotNull(); + verify(roaMetricsService).countAdded(1); } @Test public void should_reject_if_etag_does_not_match_current_configuration() { - assertThatThrownBy(() -> subject.handle(new UpdateRoaConfigurationCommand( + var command = new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.of("bad-etag"), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX, null)), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), Collections.emptyList() - ))).isInstanceOf(EntityTagDoesNotMatchException.class); + ); + assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(EntityTagDoesNotMatchException.class); } @Test(expected = PrivateAsnsUsedException.class) @@ -91,7 +93,7 @@ public void should_reject_new_additions_of_private_ASN() { subject.handle(new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.empty(), - Collections.singletonList(new RoaConfigurationPrefixData(PRIVATE_ASN, PREFIX, null)), + Collections.singletonList(new RoaConfigurationPrefixData(PRIVATE_ASN, PREFIX1, null)), Collections.emptyList())); verifyNoMoreInteractions(roaMetricsService); } @@ -109,16 +111,18 @@ public void should_reject_uncertified_prefixes() { @Test public void should_remove_deletions() { - configuration.addPrefix(Collections.singleton(new RoaConfigurationPrefix(ASN, PREFIX, null))); + var configuration = new RoaConfiguration(certificateAuthority); + configuration.addPrefixes(Collections.singleton(new RoaConfigurationPrefix(ASN, PREFIX1, null))); subject.handle(new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.empty(), Collections.emptyList(), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX, null)))); + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + + var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); + assertThat(config.getPrefixes()).isEmpty(); - assertThat(configuration.getPrefixes()).isEmpty(); - verify(roaConfigurationRepository).logRoaPrefixDeletion(configuration, Collections.singleton(new RoaConfigurationPrefix(ASN, PREFIX, null))); verify(roaMetricsService).countDeleted(1); } @@ -135,4 +139,52 @@ public void should_notify_roa_entity_service_on_configuration_change() { assertThat(certificateAuthority.isConfigurationCheckNeeded()).isTrue(); } + + @Test + public void should_replace_roa_prefix() { + var configuration = new RoaConfiguration(certificateAuthority); + subject.handle(new UpdateRoaConfigurationCommand( + certificateAuthority.getVersionedId(), + Optional.of(configuration.convertToData().entityTag()), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), + Collections.emptyList())); + + subject.handle(new UpdateRoaConfigurationCommand( + certificateAuthority.getVersionedId(), + Optional.of(roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority).convertToData().entityTag()), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX2, null)), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + + var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); + assertThat(config.getPrefixes()).hasSize(1); + RoaConfigurationPrefix p = config.getPrefixes().iterator().next(); + assertThat(p.getAsn()).isEqualTo(ASN); + assertThat(p.getPrefix()).isEqualTo(PREFIX2); + assertThat(p.getMaximumLength()).isEqualTo(12); + assertThat(p.getUpdatedAt()).isNotNull(); + } + + @Test + public void should_replace_roa_max_len() { + var configuration = new RoaConfiguration(certificateAuthority); + subject.handle(new UpdateRoaConfigurationCommand( + certificateAuthority.getVersionedId(), + Optional.of(configuration.convertToData().entityTag()), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), + Collections.emptyList())); + + subject.handle(new UpdateRoaConfigurationCommand( + certificateAuthority.getVersionedId(), + Optional.of(roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority).convertToData().entityTag()), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, 17)), + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + + var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); + assertThat(config.getPrefixes()).hasSize(1); + RoaConfigurationPrefix p = config.getPrefixes().iterator().next(); + assertThat(p.getAsn()).isEqualTo(ASN); + assertThat(p.getPrefix()).isEqualTo(PREFIX1); + assertThat(p.getMaximumLength()).isEqualTo(17); + assertThat(p.getUpdatedAt()).isNotNull(); + } } diff --git a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepositoryTest.java b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepositoryTest.java index 75f28c7..fc79ca3 100644 --- a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepositoryTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepositoryTest.java @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import jakarta.transaction.Transactional; -import java.math.BigInteger; + import java.time.Instant; import java.util.Arrays; import java.util.List; @@ -51,16 +51,15 @@ public void getOrCreateByCertificateAuthority() { } @Test - public void shouldFindAllPerCaAndCountPrefixes() { + public void shouldFindAllPrefixesAndCountPrefixes() { RoaConfiguration roaConfig = subject.getOrCreateByCertificateAuthority(ca); RoaConfigurationPrefix p1 = new RoaConfigurationPrefix(new Asn(1), IpRange.parse("10.11.0.0/16"), 16); RoaConfigurationPrefix p2 = new RoaConfigurationPrefix(new Asn(2), IpRange.parse("10.12.0.0/16"), 16); RoaConfigurationPrefix p3 = new RoaConfigurationPrefix(new Asn(3), IpRange.parse("10.13.0.0/16"), null); - roaConfig.addPrefix(Arrays.asList(p1, p2, p3)); - List allPerCa = subject.findAllPerCa(); + subject.addPrefixes(roaConfig, Arrays.asList(p1, p2, p3)); + var allPerCa = subject.findAllPrefixes(); assertNotNull(allPerCa); assertEquals(3, allPerCa.size()); - assertEquals(3, subject.countRoaPrefixes()); } @@ -77,7 +76,7 @@ public void shouldSetRecentLastModified() { RoaConfigurationPrefix p1 = new RoaConfigurationPrefix(new Asn(1), IpRange.parse("10.11.0.0/16"), 16); RoaConfigurationPrefix p2 = new RoaConfigurationPrefix(new Asn(2), IpRange.parse("10.12.0.0/16"), 16); RoaConfigurationPrefix p3 = new RoaConfigurationPrefix(new Asn(3), IpRange.parse("10.13.0.0/16"), null); - roaConfig.addPrefix(Arrays.asList(p1, p2, p3)); + roaConfig.addPrefixes(Arrays.asList(p1, p2, p3)); // And check not modified then(subject.lastModified().get()).isAfterOrEqualTo(Instant.ofEpochMilli(roaConfig.getUpdatedAt().getMillis())); @@ -88,16 +87,9 @@ public void shouldSetRecentLastModified() { */ @Test public void shouldSetRecentLastModifiedForDeletes() { - RoaConfigurationPrefix p1 = new RoaConfigurationPrefix(new Asn(1), IpRange.parse("10.11.0.0/16"), 16); - RoaConfigurationPrefix p2 = new RoaConfigurationPrefix(new Asn(2), IpRange.parse("10.12.0.0/16"), 16); - RoaConfigurationPrefix p3 = new RoaConfigurationPrefix(new Asn(3), IpRange.parse("10.13.0.0/16"), null); - final List prefixes = Arrays.asList(p1, p2, p3); - RoaConfiguration roaConfig = subject.getOrCreateByCertificateAuthority(ca); - // add prefixes since logRoaPrefixDeletion iterates over them transactionTemplate.execute((status) -> entityManager.merge(roaConfig)); - subject.logRoaPrefixDeletion(roaConfig, prefixes); // And check that not modified has updated then(subject.lastModified().get()).isAfterOrEqualTo(Instant.ofEpochMilli(roaConfig.getUpdatedAt().toInstant().getMillis())); } @@ -110,8 +102,8 @@ public void shouldInsertDeletedPrefixesToSeparateTable() { RoaConfigurationPrefix p3 = new RoaConfigurationPrefix(Asn.parse("AS12"), IpRange.parse("2a03:600::/32")); final List prefixes = Arrays.asList(p1, p2, p3); - roaConfig.addPrefix(prefixes); - subject.logRoaPrefixDeletion(roaConfig, prefixes); + subject.addPrefixes(roaConfig, prefixes); + subject.removePrefixes(roaConfig, prefixes); assertEquals(3L, countQuery("SELECT COUNT(*) FROM deleted_roaconfiguration_prefixes")); assertEquals(1L, countQuery("SELECT COUNT(*) FROM deleted_roaconfiguration_prefixes WHERE asn = 10 AND prefix_type_id = 1 AND maximum_length = 8")); @@ -119,11 +111,38 @@ public void shouldInsertDeletedPrefixesToSeparateTable() { assertEquals(1L, countQuery("SELECT COUNT(*) FROM deleted_roaconfiguration_prefixes WHERE asn = 12 AND prefix_type_id = 2 AND maximum_length = 32")); } + @Test + public void shouldInsertDeletedPrefixesToSeparateTableWhenUpdatingPrefix() { + RoaConfiguration roaConfig = subject.getOrCreateByCertificateAuthority(ca); + RoaConfigurationPrefix p1 = new RoaConfigurationPrefix(Asn.parse("AS10"), IpRange.parse("20.0.0.0/8")); + RoaConfigurationPrefix p2 = new RoaConfigurationPrefix(Asn.parse("AS11"), IpRange.parse("21.21.0.0/16")); + RoaConfigurationPrefix p3 = new RoaConfigurationPrefix(Asn.parse("AS12"), IpRange.parse("2a03:600::/32")); + + subject.addPrefixes(roaConfig, Arrays.asList(p1, p2, p3)); + + assertEquals(3L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 10 AND maximum_length = 8")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 11")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 12")); + + // replace max length 8 with 12 + subject.mergePrefixes(roaConfig, + List.of(new RoaConfigurationPrefix(Asn.parse("AS10"), IpRange.parse("20.0.0.0/8"), 12)), + List.of(new RoaConfigurationPrefix(Asn.parse("AS10"), IpRange.parse("20.0.0.0/8"), 8))); + + assertEquals(3L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 10 AND maximum_length = 12")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 11")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM roaconfiguration_prefixes WHERE asn = 12")); + + assertEquals(1L, countQuery("SELECT COUNT(*) FROM deleted_roaconfiguration_prefixes")); + assertEquals(1L, countQuery("SELECT COUNT(*) FROM deleted_roaconfiguration_prefixes WHERE asn = 10 AND prefix_type_id = 1 AND maximum_length = 8")); + } + long countQuery(String sql) { - final var count = (Long) entityManager - .createNativeQuery(sql) - .getSingleResult(); - return count.longValue(); + return (Long) entityManager + .createNativeQuery(sql) + .getSingleResult(); } } \ No newline at end of file diff --git a/src/test/java/net/ripe/rpki/util/StreamsTest.java b/src/test/java/net/ripe/rpki/util/StreamsTest.java index aa033c1..d5bb76b 100644 --- a/src/test/java/net/ripe/rpki/util/StreamsTest.java +++ b/src/test/java/net/ripe/rpki/util/StreamsTest.java @@ -4,10 +4,16 @@ import net.jqwik.api.Property; import net.jqwik.api.constraints.Positive; import net.jqwik.api.constraints.Size; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.Test; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -26,4 +32,28 @@ public void shouldGroup(@ForAll @Size(min= 3) List s, @ForAll @Positive final List concatenated = grouped.stream().flatMap(Collection::stream).toList(); assertThat(s).isEqualTo(concatenated); } + + @Test + void shouldFilterDistinctByKey_random() { + var uniqueStrings = IntStream.range(0, 26).mapToObj(String::valueOf).collect(Collectors.toList());; + Collections.shuffle(uniqueStrings); + var duplicated = Stream.concat(uniqueStrings.stream(), uniqueStrings.stream()).collect(Collectors.toList()); + + List distinct = duplicated.stream().filter(Streams.distinctByKey(Function.identity())).collect(Collectors.toList()); + assertThat(distinct).containsExactlyInAnyOrderElementsOf(uniqueStrings); + } + + @Test + void shouldFilterDistinctByKey_tuple() { + var inputs = List.of(Pair.of("A", 9), Pair.of("Z", 1), Pair.of("Y", 1)); + + // Rights are not unique + assertThat(inputs.stream().filter(Streams.distinctByKey(Pair::getRight))).hasSize(2); + + // Lefts are + assertThat(inputs.stream().filter(Streams.distinctByKey(Pair::getLeft))).hasSize(3); + + // As are the objects + assertThat(inputs.stream().filter(Streams.distinctByKey(Function.identity()))).hasSize(3); + } }