diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml
new file mode 100644
index 000000000..c7cf65200
--- /dev/null
+++ b/.github/workflows/release-beta.yml
@@ -0,0 +1,149 @@
+name: VinylDNS Beta Release
+concurrency:
+ cancel-in-progress: true
+ group: "release"
+
+defaults:
+ run:
+ shell: bash
+
+on:
+ workflow_dispatch:
+ inputs:
+ verify-first:
+ description: 'Verify First?'
+ required: true
+ default: 'true'
+ create-gh-release:
+ description: 'Create a GitHub Release?'
+ required: true
+ default: 'true'
+ publish-images:
+ description: 'Publish Docker Images?'
+ required: true
+ default: 'true'
+ pre-release:
+ description: 'Is this a pre-release?'
+ required: true
+ default: 'true'
+
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+ verify:
+ name: Verify Release
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout current branch
+ if: github.event.inputs.verify-first == 'true'
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Run Tests
+ id: verify
+ if: github.event.inputs.verify-first == 'true'
+ run: cd build/ && ./assemble_api.sh && ./run_all_tests.sh
+
+ create-gh-release:
+ name: Create GitHub Release
+ needs: verify
+ runs-on: ubuntu-latest
+ if: github.event.inputs.create-gh-release == 'true'
+ permissions:
+ contents: write
+
+ steps:
+ - name: Checkout current branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Build Artifacts
+ id: build
+ run: cd build/ && ./assemble_api.sh && ./assemble_portal.sh
+
+ - name: Get Version
+ id: get-version
+ run: echo "::set-output name=vinyldns_version::$(awk -F'"' '{print $2}' ./version.sbt)"
+
+ - name: Create GitHub Release
+ id: create_release
+ uses: softprops/action-gh-release@v2
+ with:
+ tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
+ generate_release_notes: true
+ files: artifacts/*
+ prerelease: ${{ github.event.inputs['pre-release'] == 'true' }}
+
+ docker-release-api:
+ name: Release API Docker Image
+ needs: [ verify, create-gh-release ]
+ runs-on: ubuntu-latest
+ if: github.event.inputs.publish-images == 'true'
+
+ steps:
+ - name: Get Version
+ id: get-version
+ run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
+
+ - name: Checkout current branch (full)
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.get-version.outputs.vinyldns_version }}
+ fetch-depth: 0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USER }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Import Content Trust Key
+ run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
+ env:
+ SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
+
+ # This will publish the latest release
+ - name: Publish API Docker Image
+ run: make -C build/docker/api publish
+ env:
+ DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
+
+ docker-release-portal:
+ name: Release Portal Docker Image
+ needs: [ verify, create-gh-release ]
+ runs-on: ubuntu-latest
+ if: github.event.inputs.publish-images == 'true'
+
+ steps:
+ - name: Get Version
+ id: get-version
+ run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
+
+ - name: Checkout current branch (full)
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.get-version.outputs.vinyldns_version }}
+ fetch-depth: 0
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USER }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Import Content Trust Key
+ run: docker trust key load <(echo "${SIGNING_KEY}") --name vinyldns_svc
+ env:
+ SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
+
+ # This will publish the latest release
+ - name: Publish Portal Docker Image
+ run: make -C build/docker/portal publish
+ env:
+ DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ secrets.DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d1ce26c22..4a4113686 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout current branch
if: github.event.inputs.verify-first == 'true'
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout current branch
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -67,7 +67,7 @@ jobs:
- name: Create GitHub Release
id: create_release
- uses: softprops/action-gh-release@1e07f4398721186383de40550babbdf2b84acfc5 # v0.1.14
+ uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.get-version.outputs.vinyldns_version }}
generate_release_notes: true
@@ -85,13 +85,13 @@ jobs:
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
- name: Checkout current branch (full)
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0
- name: Login to Docker Hub
- uses: docker/login-action@v1
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
@@ -120,13 +120,13 @@ jobs:
run: echo "::set-output name=vinyldns_version::$(curl -s https://api.github.com/repos/vinyldns/vinyldns/releases | jq -rc '.[0].tag_name')"
- name: Checkout current branch (full)
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
ref: ${{ steps.get-version.outputs.vinyldns_version }}
fetch-depth: 0
- name: Login to Docker Hub
- uses: docker/login-action@v1
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index 126b64c90..c46ac0f20 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -31,28 +31,28 @@ jobs:
- name: Codecov
id: codecov0
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry
id: codecov1
if: steps.codecov0.outcome=='failure'
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry 2
id: codecov2
if: steps.codecov1.outcome=='failure'
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Codecov Retry 3
id: codecov3
if: steps.codecov2.outcome=='failure'
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v4
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/AUTHORS.md b/AUTHORS.md
index 2e713ac03..a634a5dd7 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -21,12 +21,14 @@ in any way, but do not see your name here, please open a PR to add yourself (in
- Joe Crowe
- Jearvon Dharrie
- Andrew Dunn
+- Josh Edwards
- Ryan Emerle
- David Grizzanti
- Alejandro Guirao
- Daniel Jin
- Harry Kauffman
- Krista Khare
+- Sokitha Krishnan
- Patrick Lee
- Sheree Liu
- Michael Ly
@@ -41,8 +43,9 @@ in any way, but do not see your name here, please open a PR to add yourself (in
- Khalid Reid
- Timo Schmid
- Trent Schmidt
-- Nick Spadaccino
+- Arpit Shah
- Ghafar Shah
+- Nick Spadaccino
- Rebecca Star
- Jess Stodola
- Juan Valencia
diff --git a/README.md b/README.md
index 1759bc105..d4e857fd5 100644
--- a/README.md
+++ b/README.md
@@ -145,9 +145,9 @@ See the [Contributing Guide](CONTRIBUTING.md).
The current maintainers (people who can merge pull requests) are:
-- Ryan Emerle ([@remerle](https://github.com/remerle))
-- Sriram Ramakrishnan ([@sramakr](https://github.com/sramakr))
-- Jim Wakemen ([@jwakemen](https://github.com/jwakemen))
+- Arpit Shah ([@arpit4ever](https://github.com/arpit4ever))
+- Nick Spadaccino ([@nspadaccino](https://github.com/nspadaccino))
+- Jay Velkumar ([@Jay07GIT](https://github.com/Jay07GIT))
See [AUTHORS.md](AUTHORS.md) for the full list of contributors to VinylDNS.
diff --git a/build.sbt b/build.sbt
index b9b5ea48c..f1678e70e 100644
--- a/build.sbt
+++ b/build.sbt
@@ -72,6 +72,9 @@ lazy val apiAssemblySettings = Seq(
MergeStrategy.discard
case PathList("scala", "tools", "nsc", "doc", "html", "resource", "lib", "template.js") =>
MergeStrategy.discard
+ case "simulacrum/op.class" | "simulacrum/op$.class" | "simulacrum/typeclass$.class"
+ | "simulacrum/typeclass.class" | "simulacrum/noop.class" =>
+ MergeStrategy.discard
case x if x.endsWith("module-info.class") => MergeStrategy.discard
case x =>
val oldStrategy = (assemblyMergeStrategy in assembly).value
diff --git a/build/docker/api/application.conf b/build/docker/api/application.conf
index e87fef2ab..6a5dd8732 100644
--- a/build/docker/api/application.conf
+++ b/build/docker/api/application.conf
@@ -27,10 +27,16 @@ vinyldns {
scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED}
multi-record-batch-change-enabled = true
multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED}
+
+ # Server settings
use-recordset-cache = true
use-recordset-cache = ${?USE_RECORDSET_CACHE}
load-test-data = false
load-test-data = ${?LOAD_TEST_DATA}
+ # should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
+ is-zone-sync-schedule-allowed = true
+ # should be set to true only on a single server/instance else automated sync will be performed at every server/instance
+ is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
# configured backend providers
backend {
@@ -155,6 +161,17 @@ vinyldns {
port=${?API_SERVICE_PORT}
}
+ api {
+ limits {
+ batchchange-routing-max-items-limit = 100
+ membership-routing-default-max-items = 100
+ membership-routing-max-items-limit = 1000
+ membership-routing-max-groups-list-limit = 3000
+ recordset-routing-default-max-items= 100
+ zone-routing-default-max-items = 100
+ zone-routing-max-items-limit = 100
+ }
+ }
approved-name-servers = [
"172.17.42.1.",
@@ -182,9 +199,9 @@ vinyldns {
name = ${?DATABASE_NAME}
driver = "org.mariadb.jdbc.Driver"
driver = ${?JDBC_DRIVER}
- migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass"
+ migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass&socketTimeout=20000"
migration-url = ${?JDBC_MIGRATION_URL}
- url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass"
+ url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass&socketTimeout=20000"
url = ${?JDBC_URL}
user = "root"
user = ${?JDBC_USER}
@@ -192,6 +209,12 @@ vinyldns {
password = ${?JDBC_PASSWORD}
flyway-out-of-order = false
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
+
+ max-lifetime = 300000
+ connection-timeout-millis = 30000
+ idle-timeout = 150000
+ maximum-pool-size = 20
+ minimum-idle = 5
}
# TODO: Remove the need for these useless configuration blocks
@@ -328,6 +351,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
+ # A default request timeout is applied globally to all routes and can be configured using the
+ # akka.http.server.request-timeout setting (which defaults to 20 seconds).
+ # request-timeout = 60s
+
# Show verbose error messages back to the client
verbose-error-messages = on
}
diff --git a/build/docker/api/logback.xml b/build/docker/api/logback.xml
index 541ca7bce..656ee3ab1 100644
--- a/build/docker/api/logback.xml
+++ b/build/docker/api/logback.xml
@@ -9,10 +9,15 @@
+
+
+
+
+
diff --git a/build/docker/portal/application.conf b/build/docker/portal/application.conf
index 48ca474b6..e691be760 100644
--- a/build/docker/portal/application.conf
+++ b/build/docker/portal/application.conf
@@ -50,9 +50,9 @@ mysql {
name = ${?DATABASE_NAME}
driver = "org.mariadb.jdbc.Driver"
driver = ${?JDBC_DRIVER}
- migration-url = "jdbc:mariadb://"${mysql.endpoint}"/?user=root&password=pass"
+ migration-url = "jdbc:mariadb://"${mysql.endpoint}"/?user=root&password=pass&socketTimeout=20000"
migration-url = ${?JDBC_MIGRATION_URL}
- url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns?user=root&password=pass"
+ url = "jdbc:mariadb://"${mysql.endpoint}"/vinyldns?user=root&password=pass&socketTimeout=20000"
url = ${?JDBC_URL}
user = "root"
user = ${?JDBC_USER}
@@ -60,6 +60,12 @@ mysql {
password = ${?JDBC_PASSWORD}
flyway-out-of-order = false
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
+
+ max-lifetime = 300000
+ connection-timeout-millis = 30000
+ idle-timeout = 150000
+ maximum-pool-size = 20
+ minimum-idle = 5
}
}
@@ -70,6 +76,18 @@ crypto {
secret = ${?CRYPTO_SECRET}
}
+api {
+ limits {
+ batchchange-routing-max-items-limit = 100
+ membership-routing-default-max-items = 100
+ membership-routing-max-items-limit = 1000
+ membership-routing-max-groups-list-limit = 3000
+ recordset-routing-default-max-items= 100
+ zone-routing-default-max-items = 100
+ zone-routing-max-items-limit = 100
+ }
+}
+
http.port = 9001
http.port = ${?PORTAL_PORT}
@@ -83,6 +101,12 @@ shared-display-enabled = ${?SHARED_ZONES_ENABLED}
play.http.secret.key = "changeme"
play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY}
+# See https://www.playframework.com/documentation/2.8.x/AllowedHostsFilter for more details.
+# Note: allowed = ["."] matches all hosts hence would not be recommended in a production environment.
+play.filters.hosts {
+ allowed = ["."]
+}
+
# You can provide configuration overrides via local.conf if you don't want to replace everything in
# this configuration file
include "local.conf"
diff --git a/build/docker/portal/logback.xml b/build/docker/portal/logback.xml
index 86b11f6d7..42e9a8b80 100644
--- a/build/docker/portal/logback.xml
+++ b/build/docker/portal/logback.xml
@@ -15,6 +15,10 @@
+
+
+
+
diff --git a/modules/api/src/it/resources/application.conf b/modules/api/src/it/resources/application.conf
index dd3b7006e..f96a2c5e5 100644
--- a/modules/api/src/it/resources/application.conf
+++ b/modules/api/src/it/resources/application.conf
@@ -348,6 +348,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
+ # A default request timeout is applied globally to all routes and can be configured using the
+ # akka.http.server.request-timeout setting (which defaults to 20 seconds).
+ # request-timeout = 60s
+
# Show verbose error messages back to the client
verbose-error-messages = on
}
diff --git a/modules/api/src/it/scala/vinyldns/api/MySqlApiIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/MySqlApiIntegrationSpec.scala
index c5863e7de..a29af80c1 100644
--- a/modules/api/src/it/scala/vinyldns/api/MySqlApiIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/MySqlApiIntegrationSpec.scala
@@ -34,6 +34,6 @@ trait MySqlApiIntegrationSpec extends MySqlIntegrationSpec {
def clearGroupRepo(): Unit =
DB.localTx { s =>
- s.executeUpdate("DELETE FROM groups")
+ s.executeUpdate("DELETE FROM `groups`")
}
}
diff --git a/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala
index 130fa04cd..98a3f9e6b 100644
--- a/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/backend/dns/DnsBackendIntegrationSpec.scala
@@ -22,14 +22,8 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import vinyldns.api.backend.dns.DnsProtocol.NoError
import vinyldns.core.crypto.NoOpCrypto
-import vinyldns.core.domain.record.{
- AData,
- RecordSet,
- RecordSetChange,
- RecordSetChangeType,
- RecordSetStatus,
- RecordType
-}
+import vinyldns.core.domain.Encrypted
+import vinyldns.core.domain.record.{AData, RecordSet, RecordSetChange, RecordSetChangeType, RecordSetStatus, RecordType}
import vinyldns.core.domain.zone.{Algorithm, Zone, ZoneConnection}
class DnsBackendIntegrationSpec extends AnyWordSpec with Matchers {
@@ -37,7 +31,7 @@ class DnsBackendIntegrationSpec extends AnyWordSpec with Matchers {
private val testConnection = ZoneConnection(
"vinyldns.",
"vinyldns.",
- "nzisn+4G2ldMn0q1CV3vsg==",
+ Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"),
Algorithm.HMAC_MD5
)
diff --git a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala
index f5daade4e..3eaa2885f 100644
--- a/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/domain/record/RecordSetServiceIntegrationSpec.scala
@@ -19,6 +19,7 @@ package vinyldns.api.domain.record
import cats.effect._
import cats.implicits._
import cats.scalatest.EitherMatchers
+import org.mockito.Matchers.any
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.mockito.Mockito._
@@ -34,13 +35,16 @@ import vinyldns.api.domain.zone._
import vinyldns.api.engine.TestMessageQueue
import vinyldns.mysql.TransactionProvider
import vinyldns.core.TestZoneData.testConnection
-import vinyldns.core.domain.{Fqdn, HighValueDomainError}
+import vinyldns.core.domain.{Encrypted, Fqdn, HighValueDomainError}
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.backend.{Backend, BackendResolver}
import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository}
import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
+import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier}
+
+import scala.concurrent.ExecutionContext
class RecordSetServiceIntegrationSpec
extends AnyWordSpec
@@ -53,19 +57,25 @@ class RecordSetServiceIntegrationSpec
with BeforeAndAfterAll
with TransactionProvider {
+ private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
+
private val vinyldnsConfig = VinylDNSConfig.load().unsafeRunSync()
private val recordSetRepo = recordSetRepository
private val recordSetCacheRepo = recordSetCacheRepository
+ private val mockNotifier = mock[Notifier]
+ private val mockNotifiers = AllNotifiers(List(mockNotifier))
+
private val zoneRepo: ZoneRepository = zoneRepository
private val groupRepo: GroupRepository = groupRepository
private var testRecordSetService: RecordSetServiceAlgebra = _
- private val user = User("live-test-user", "key", "secret")
- private val testUser = User("testuser", "key", "secret")
- private val user2 = User("shared-record-test-user", "key-shared", "secret-shared")
+ private val user = User("live-test-user", "key", Encrypted("secret"))
+ private val testUser = User("testuser", "key", Encrypted("secret"))
+ private val user2 = User("shared-record-test-user", "key-shared", Encrypted("secret-shared"))
+
private val group = Group(s"test-group", "test@test.com", adminUserIds = Set(user.id))
private val dummyGroup = Group(s"dummy-group", "test@test.com", adminUserIds = Set(testUser.id))
private val group2 = Group(s"test-group", "test@test.com", adminUserIds = Set(user.id, user2.id))
@@ -118,7 +128,10 @@ class RecordSetServiceIntegrationSpec
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(AAAAData("fd69:27cc:fe91::60"))
+ List(AAAAData("fd69:27cc:fe91::60")),
+ recordSetGroupChange =
+ Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None,
+ requestedOwnerGroupId = None))
)
private val subTestRecordA = RecordSet(
zone.id,
@@ -231,6 +244,36 @@ class RecordSetServiceIntegrationSpec
ownerGroupId = Some("non-existent")
)
+ private val sharedTestRecordPendingReviewOwnerShip = RecordSet(
+ sharedZone.id,
+ "shared-record-ownerShip-pendingReview",
+ A,
+ 200,
+ RecordSetStatus.Active,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ None,
+ List(AData("1.1.1.1")),
+ ownerGroupId = Some(sharedGroup.id),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,
+ requestedOwnerGroupId = Some(group.id)))
+ )
+
+ private val sharedTestRecordCancelledOwnerShip = RecordSet(
+ sharedZone.id,
+ "shared-record-ownerShip-cancelled",
+ A,
+ 200,
+ RecordSetStatus.Active,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ None,
+ List(AData("1.1.1.1")),
+ ownerGroupId = Some(sharedGroup.id),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
+ requestedOwnerGroupId = Some(group.id)))
+ )
+
private val testOwnerGroupRecordInNormalZone = RecordSet(
zone.id,
"user-in-owner-group-but-zone-not-shared",
@@ -284,7 +327,10 @@ class RecordSetServiceIntegrationSpec
// Seeding records in DB
val sharedRecords = List(
sharedTestRecord,
- sharedTestRecordBadOwnerGroup
+ sharedTestRecordBadOwnerGroup,
+ sharedTestRecordPendingReviewOwnerShip,
+ sharedTestRecordCancelledOwnerShip
+
)
val conflictRecords = List(
subTestRecordNameConflict,
@@ -323,7 +369,8 @@ class RecordSetServiceIntegrationSpec
vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers,
- useRecordSetCache = true
+ useRecordSetCache = true,
+ mockNotifiers
)
}
@@ -424,6 +471,227 @@ class RecordSetServiceIntegrationSpec
leftValue(result) shouldBe a[InvalidRequest]
}
+ "auto-approve ownership transfer request, if user tried to update the ownership" in {
+ val newRecord = sharedTestRecord.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
+ requestedOwnerGroupId = Some(group.id))))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth2)
+ .value
+ .unsafeRunSync()
+
+ val change = rightValue(result).asInstanceOf[RecordSetChange]
+ change.recordSet.name shouldBe "shared-record"
+ change.recordSet.ownerGroupId.get shouldBe group.id
+ change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved
+ change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
+ }
+
+ "approve ownership transfer request, if user requested for ownership transfer" in {
+ val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth2)
+ .value
+ .unsafeRunSync()
+
+ val change = rightValue(result).asInstanceOf[RecordSetChange]
+ change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
+ change.recordSet.ownerGroupId.get shouldBe group.id
+ change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved
+ change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
+ }
+
+ "reject ownership transfer request, if user requested for ownership transfer" in {
+ val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth2)
+ .value
+ .unsafeRunSync()
+
+ val change = rightValue(result).asInstanceOf[RecordSetChange]
+ change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
+ change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
+ change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected
+ change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
+ }
+
+ "request ownership transfer, if user not in the owner group and wants to own the record" in {
+ val newRecord = sharedTestRecord.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
+ requestedOwnerGroupId = Some(dummyGroup.id))))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, dummyAuth)
+ .value
+ .unsafeRunSync()
+
+ val change = rightValue(result).asInstanceOf[RecordSetChange]
+ change.recordSet.name shouldBe "shared-record"
+ change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
+ change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview
+ change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe dummyGroup.id
+ }
+
+ "fail requesting ownership transfer if user is not in owner group and tried to update other fields in record set" in {
+ val newRecord = sharedTestRecord.copy(
+ ttl = 3000,
+ recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
+ requestedOwnerGroupId = Some(dummyGroup.id))))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, dummyAuth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail updating if user is not in owner group for ownership transfer approval" in {
+ val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, dummyAuth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[NotAuthorizedError]
+ }
+
+ "fail updating if user is not in owner group for ownership transfer reject" in {
+ val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, dummyAuth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[NotAuthorizedError]
+ }
+
+ "cancel the ownership transfer request, if user not require ownership transfer further" in {
+ val newRecord = sharedTestRecordPendingReviewOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth)
+ .value
+ .unsafeRunSync()
+
+ val change = rightValue(result).asInstanceOf[RecordSetChange]
+ change.recordSet.name shouldBe "shared-record-ownerShip-pendingReview"
+ change.recordSet.ownerGroupId.get shouldBe sharedGroup.id
+ change.recordSet.recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled
+ change.recordSet.recordSetGroupChange.get.requestedOwnerGroupId.get shouldBe group.id
+ }
+
+ "fail approving ownership transfer request, if user is cancelled" in {
+ val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail rejecting ownership transfer request, if user is cancelled" in {
+ val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail auto-approving ownership transfer request, if user is cancelled" in {
+ val newRecord = sharedTestRecordCancelledOwnerShip.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved
+ )))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail auto-approving ownership transfer request, if zone is not shared" in {
+ val newRecord = dottedTestRecord.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
+ requestedOwnerGroupId = Some(group.id))))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth2)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail approving ownership transfer request, if zone is not shared" in {
+ val newRecord = dottedTestRecord.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved
+ )))
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, auth2)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
+ "fail requesting ownership transfer, if zone is not shared" in {
+ val newRecord = dottedTestRecord.copy(recordSetGroupChange =
+ Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
+ requestedOwnerGroupId = Some(dummyGroup.id)
+ )))
+
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = testRecordSetService
+ .updateRecordSet(newRecord, dummyAuth)
+ .value
+ .unsafeRunSync()
+
+ leftValue(result) shouldBe a[InvalidRequest]
+ }
+
"update dotted record succeeds if it satisfies all dotted hosts config" in {
val newRecord = dottedTestRecord.copy(ttl = 37000)
@@ -668,25 +936,23 @@ class RecordSetServiceIntegrationSpec
}
"fail deleting for user not in record owner group in shared zone" in {
- val result = leftResultOf(
+ val result =
testRecordSetService
.deleteRecordSet(sharedTestRecord.id, sharedTestRecord.zoneId, dummyAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
"fail deleting for user in record owner group in non-shared zone" in {
- val result = leftResultOf(
+ val result =
testRecordSetService
.deleteRecordSet(
testOwnerGroupRecordInNormalZone.id,
testOwnerGroupRecordInNormalZone.zoneId,
auth2
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
diff --git a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneServiceIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneServiceIntegrationSpec.scala
index 66712ca73..f2cf01dae 100644
--- a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneServiceIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneServiceIntegrationSpec.scala
@@ -18,6 +18,7 @@ package vinyldns.api.domain.zone
import cats.data.NonEmptyList
import cats.effect._
+
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.mockito.Mockito.doReturn
@@ -29,6 +30,7 @@ import org.scalatestplus.mockito.MockitoSugar
import org.scalatest.time.{Seconds, Span}
import scalikejdbc.DB
import vinyldns.api.domain.access.AccessValidations
+import vinyldns.api.domain.membership.MembershipService
import vinyldns.api.domain.record.RecordSetChangeGenerator
import vinyldns.api.engine.TestMessageQueue
import vinyldns.mysql.TransactionProvider
@@ -62,7 +64,7 @@ class ZoneServiceIntegrationSpec
private val recordSetRepo = recordSetRepository
private val zoneRepo: ZoneRepository = zoneRepository
-
+ private val mockMembershipService = mock[MembershipService]
private var testZoneService: ZoneServiceAlgebra = _
private val badAuth = AuthPrincipal(okUser, Seq())
@@ -127,7 +129,8 @@ class ZoneServiceIntegrationSpec
new ZoneValidations(1000),
new AccessValidations(),
mockBackendResolver,
- NoOpCrypto.instance
+ NoOpCrypto.instance,
+ mockMembershipService
)
}
diff --git a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala
index b757d5518..f319856d8 100644
--- a/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/domain/zone/ZoneViewLoaderIntegrationSpec.scala
@@ -19,8 +19,8 @@ package vinyldns.api.domain.zone
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.xbill.DNS.ZoneTransferException
-import vinyldns.api.backend.dns.DnsBackend
import vinyldns.api.config.VinylDNSConfig
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.backend.BackendResolver
import vinyldns.core.domain.zone.{Zone, ZoneConnection}
@@ -50,15 +50,13 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
ZoneConnection(
"vinyldns.",
"vinyldns.",
- "nzisn+4G2ldMn0q1CV3vsg==",
+ Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
)
),
transferConnection =
- Some(ZoneConnection("invalid-connection.", "bad-key", "invalid-key", "10.1.1.1"))
+ Some(ZoneConnection("invalid-connection.", "bad-key", Encrypted("invalid-key"), "10.1.1.1"))
)
- val backend = backendResolver.resolve(zone).asInstanceOf[DnsBackend]
- println(s"${backend.id}, ${backend.xfrInfo}, ${backend.resolver.getAddress}")
DnsZoneViewLoader(zone, backendResolver.resolve(zone), 10000)
.load()
.unsafeRunSync()
@@ -83,7 +81,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
ZoneConnection(
"vinyldns.",
"vinyldns.",
- "nzisn+4G2ldMn0q1CV3vsg==",
+ Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
)
),
@@ -91,7 +89,7 @@ class ZoneViewLoaderIntegrationSpec extends AnyWordSpec with Matchers {
ZoneConnection(
"vinyldns.",
"vinyldns.",
- "nzisn+4G2ldMn0q1CV3vsg==",
+ Encrypted("nzisn+4G2ldMn0q1CV3vsg=="),
sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001")
)
)
diff --git a/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala
index f28168966..b1bb4116c 100644
--- a/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/notifier/email/EmailNotifierIntegrationSpec.scala
@@ -24,7 +24,7 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpecLike
import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType
-import vinyldns.core.domain.record.AData
+import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
import java.time.Instant
import java.time.temporal.ChronoUnit
import vinyldns.core.TestMembershipData._
@@ -35,6 +35,8 @@ import cats.effect.{IO, Resource}
import scala.collection.JavaConverters._
import org.scalatest.BeforeAndAfterEach
import cats.implicits._
+import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk}
+import vinyldns.core.TestZoneData.okZone
class EmailNotifierIntegrationSpec
extends MySqlApiIntegrationSpec
@@ -57,7 +59,7 @@ class EmailNotifierIntegrationSpec
"Email Notifier" should {
- "send an email" taggedAs (SkipCI) in {
+ "send an email for batch change" taggedAs (SkipCI) in {
val batchChange = BatchChange(
okUser.id,
okUser.userName,
@@ -84,7 +86,7 @@ class EmailNotifierIntegrationSpec
val program = for {
_ <- userRepository.save(okUser)
notifier <- new EmailNotifierProvider()
- .load(NotifierConfig("", emailConfig), userRepository)
+ .load(NotifierConfig("", emailConfig), userRepository, groupRepository)
_ <- notifier.notify(Notification(batchChange))
emailFiles <- retrieveEmailFiles(targetDirectory)
} yield emailFiles
@@ -94,7 +96,29 @@ class EmailNotifierIntegrationSpec
files.length should be(1)
}
+ "send an email for recordSetChange ownerShip transfer" taggedAs (SkipCI) in {
+ val recordSetChange = RecordSetChange(
+ okZone,
+ rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))),
+ "system",
+ RecordSetChangeType.Create,
+ RecordSetChangeStatus.Complete
+ )
+ val program = for {
+ _ <- userRepository.save(okUser)
+ notifier <- new EmailNotifierProvider()
+ .load(NotifierConfig("", emailConfig), userRepository, groupRepository)
+ _ <- notifier.notify(Notification(recordSetChange))
+ emailFiles <- retrieveEmailFiles(targetDirectory)
+ } yield emailFiles
+
+ val files = program.unsafeRunSync()
+
+ files.length should be(1)
+
+ }
}
def deleteEmailFiles(path: Path): IO[Unit] =
diff --git a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala
index a10a558a2..5fb7b41e0 100644
--- a/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/notifier/sns/SnsNotifierIntegrationSpec.scala
@@ -111,7 +111,7 @@ class SnsNotifierIntegrationSpec
sns.subscribe(topic, "sqs", queueUrl)
}
notifier <- new SnsNotifierProvider()
- .load(NotifierConfig("", snsConfig), userRepository)
+ .load(NotifierConfig("", snsConfig), userRepository, groupRepository)
_ <- notifier.notify(Notification(batchChange))
_ <- IO.sleep(1.seconds)
messages <- IO {
diff --git a/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala b/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala
index 29d1bf320..d1dee829f 100644
--- a/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala
+++ b/modules/api/src/it/scala/vinyldns/api/route53/Route53ApiIntegrationSpec.scala
@@ -30,7 +30,7 @@ import vinyldns.api.engine.ZoneSyncHandler
import vinyldns.api.{MySqlApiIntegrationSpec, ResultHelpers}
import vinyldns.core.TestRecordSetData._
import vinyldns.core.domain.backend.{Backend, BackendResolver}
-import vinyldns.core.domain.record.{NameSort, RecordType}
+import vinyldns.core.domain.record.{NameSort, RecordType, RecordTypeSort}
import vinyldns.core.domain.zone.{Zone, ZoneChange, ZoneChangeType}
import vinyldns.core.health.HealthCheck.HealthCheck
import vinyldns.route53.backend.{Route53Backend, Route53BackendConfig}
@@ -57,6 +57,8 @@ class Route53ApiIntegrationSpec
"test",
Some("access"),
Some("secret"),
+ None,
+ None,
sys.env.getOrElse("R53_SERVICE_ENDPOINT", "http://localhost:19003"),
"us-east-1"
)
@@ -120,7 +122,7 @@ class Route53ApiIntegrationSpec
// We should have both the record we created above as well as at least one NS record
val results = recordSetRepository
- .listRecordSets(Some(testZone.id), None, None, None, None, None, NameSort.ASC)
+ .listRecordSets(Some(testZone.id), None, None, None, None, None, NameSort.ASC, RecordTypeSort.ASC)
.unsafeRunSync()
results.recordSets.map(_.typ).distinct should contain theSameElementsAs List(
rsOk.typ,
diff --git a/modules/api/src/main/resources/application.conf b/modules/api/src/main/resources/application.conf
index 7407c07e4..68b519c26 100644
--- a/modules/api/src/main/resources/application.conf
+++ b/modules/api/src/main/resources/application.conf
@@ -27,10 +27,16 @@ vinyldns {
scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED}
multi-record-batch-change-enabled = true
multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED}
+
+ # Server settings
use-recordset-cache = false
use-recordset-cache = ${?USE_RECORDSET_CACHE}
load-test-data = false
load-test-data = ${?LOAD_TEST_DATA}
+ # should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
+ is-zone-sync-schedule-allowed = true
+ # should be set to true only on a single server/instance else automated sync will be performed at every server/instance
+ is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
# configured backend providers
backend {
@@ -131,7 +137,10 @@ vinyldns {
from = ${?EMAIL_FROM}
}
}
-
+ valid-email-config{
+ email-domains = ["test.com","*dummy.com"]
+ number-of-dots= 2
+ }
sns {
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"
class-name = ${?SNS_CLASS_NAME}
@@ -155,6 +164,17 @@ vinyldns {
port=${?API_SERVICE_PORT}
}
+ api {
+ limits {
+ batchchange-routing-max-items-limit = 100
+ membership-routing-default-max-items = 100
+ membership-routing-max-items-limit = 1000
+ membership-routing-max-groups-list-limit = 3000
+ recordset-routing-default-max-items= 100
+ zone-routing-default-max-items = 100
+ zone-routing-max-items-limit = 100
+ }
+ }
approved-name-servers = [
"172.17.42.1.",
@@ -195,9 +215,9 @@ vinyldns {
name = ${?DATABASE_NAME}
driver = "org.mariadb.jdbc.Driver"
driver = ${?JDBC_DRIVER}
- migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass"
+ migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass&socketTimeout=20000"
migration-url = ${?JDBC_MIGRATION_URL}
- url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass"
+ url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass&socketTimeout=20000"
url = ${?JDBC_URL}
user = "root"
user = ${?JDBC_USER}
@@ -205,6 +225,12 @@ vinyldns {
password = ${?JDBC_PASSWORD}
flyway-out-of-order = false
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
+
+ max-lifetime = 300000
+ connection-timeout-millis = 30000
+ idle-timeout = 150000
+ maximum-pool-size = 20
+ minimum-idle = 5
}
# TODO: Remove the need for these useless configuration blocks
@@ -341,6 +367,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
+ # A default request timeout is applied globally to all routes and can be configured using the
+ # akka.http.server.request-timeout setting (which defaults to 20 seconds).
+ # request-timeout = 60s
+
# Show verbose error messages back to the client
verbose-error-messages = on
}
diff --git a/modules/api/src/main/resources/logback.xml b/modules/api/src/main/resources/logback.xml
index 4f55b5431..4e4f1f6aa 100644
--- a/modules/api/src/main/resources/logback.xml
+++ b/modules/api/src/main/resources/logback.xml
@@ -7,6 +7,10 @@
+
+
+
+
diff --git a/modules/api/src/main/resources/reference.conf b/modules/api/src/main/resources/reference.conf
index 4d1db46a1..817c668e1 100644
--- a/modules/api/src/main/resources/reference.conf
+++ b/modules/api/src/main/resources/reference.conf
@@ -169,7 +169,10 @@ vinyldns {
from = "VinylDNS "
}
}
-
+ valid-email-config{
+ email-domains = ["test.com","*dummy.com"]
+ number-of-dots= 2
+ }
sns {
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"
settings {
@@ -232,4 +235,9 @@ vinyldns {
load-test-data = false
load-test-data = ${?LOAD_TEST_DATA}
+
+ # should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
+ is-zone-sync-schedule-allowed = true
+ # should be set to true only on a single server/instance else automated sync will be performed at every server/instance
+ is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/Boot.scala b/modules/api/src/main/scala/vinyldns/api/Boot.scala
index ae4077f49..7319c1289 100644
--- a/modules/api/src/main/scala/vinyldns/api/Boot.scala
+++ b/modules/api/src/main/scala/vinyldns/api/Boot.scala
@@ -20,6 +20,7 @@ import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.stream.{Materializer, ActorMaterializer}
import cats.effect.{Timer, IO, ContextShift}
+import cats.data.NonEmptyList
import com.typesafe.config.ConfigFactory
import fs2.concurrent.SignallingRef
import io.prometheus.client.CollectorRegistry
@@ -46,10 +47,15 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.io.{Codec, Source}
import vinyldns.core.notifier.NotifierLoader
import vinyldns.core.repository.DataStoreLoader
+import java.util.concurrent.{Executors, ScheduledExecutorService, TimeUnit}
object Boot extends App {
private val logger = LoggerFactory.getLogger("Boot")
+
+ // Create a ScheduledExecutorService with a new single thread
+ private val executor: ScheduledExecutorService = Executors.newSingleThreadScheduledExecutor()
+
private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global
private implicit val cs: ContextShift[IO] = IO.contextShift(ec)
private implicit val timer: Timer[IO] = IO.timer(ec)
@@ -90,9 +96,23 @@ object Boot extends App {
msgsPerPoll <- IO.fromEither(MessageCount(vinyldnsConfig.messageQueueConfig.messagesPerPoll))
notifiers <- NotifierLoader.loadAll(
vinyldnsConfig.notifierConfigs,
- repositories.userRepository
+ repositories.userRepository,
+ repositories.groupRepository
)
_ <- APIMetrics.initialize(vinyldnsConfig.apiMetricSettings)
+ // Schedule the zone sync task to be executed every 5 seconds
+ _ <- if (vinyldnsConfig.serverConfig.isZoneSyncScheduleAllowed){ IO(executor.scheduleAtFixedRate(() => {
+ val zoneChanges = for {
+ zoneChanges <- ZoneSyncScheduleHandler.zoneSyncScheduler(repositories.zoneRepository)
+ _ <- if (zoneChanges.nonEmpty) messageQueue.sendBatch(NonEmptyList.fromList(zoneChanges.toList).get) else IO.unit
+ } yield ()
+ zoneChanges.unsafeRunAsync {
+ case Right(_) =>
+ logger.debug("Zone sync scheduler ran successfully!")
+ case Left(error) =>
+ logger.error(s"An error occurred while performing the scheduled zone sync. Error: $error")
+ }
+ }, 0, 1, TimeUnit.SECONDS)) } else IO.unit
_ <- CommandHandler.run(
messageQueue,
msgsPerPoll,
@@ -121,9 +141,10 @@ object Boot extends App {
vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.manualReviewConfig,
vinyldnsConfig.batchChangeConfig,
- vinyldnsConfig.scheduledChangesConfig
+ vinyldnsConfig.scheduledChangesConfig,
+ vinyldnsConfig.serverConfig.approvedNameServers
)
- val membershipService = MembershipService(repositories)
+ val membershipService = MembershipService(repositories,vinyldnsConfig.validEmailConfig)
val connectionValidator =
new ZoneConnectionValidator(
@@ -141,7 +162,8 @@ object Boot extends App {
vinyldnsConfig.highValueDomainConfig,
vinyldnsConfig.dottedHostsConfig,
vinyldnsConfig.serverConfig.approvedNameServers,
- vinyldnsConfig.serverConfig.useRecordSetCache
+ vinyldnsConfig.serverConfig.useRecordSetCache,
+ notifiers
)
val zoneService = ZoneService(
repositories,
@@ -150,7 +172,8 @@ object Boot extends App {
zoneValidations,
recordAccessValidations,
backendResolver,
- vinyldnsConfig.crypto
+ vinyldnsConfig.crypto,
+ membershipService
)
//limits configured in reference.conf passing here
val limits = LimitsConfig(
diff --git a/modules/api/src/main/scala/vinyldns/api/backend/CommandHandler.scala b/modules/api/src/main/scala/vinyldns/api/backend/CommandHandler.scala
index 1eeaf3c7a..797328494 100644
--- a/modules/api/src/main/scala/vinyldns/api/backend/CommandHandler.scala
+++ b/modules/api/src/main/scala/vinyldns/api/backend/CommandHandler.scala
@@ -39,6 +39,7 @@ import vinyldns.core.queue.{CommandMessage, MessageCount, MessageQueue}
import scala.concurrent.duration._
import vinyldns.core.notifier.AllNotifiers
+import java.io.{PrintWriter, StringWriter}
object CommandHandler {
@@ -94,7 +95,9 @@ object CommandHandler {
)
.parJoin(maxOpen)
.handleErrorWith { error =>
- logger.error("Encountered unexpected error in main flow", error)
+ val errorMessage = new StringWriter
+ error.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Encountered unexpected error in main flow. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
// just continue, the flow should never stop unless explicitly told to do so
flow()
@@ -123,7 +126,9 @@ object CommandHandler {
.handleErrorWith { error =>
// on error, we make sure we still continue; should only stop when the app stops
// or processing is disabled
- logger.error("Encountered error polling message queue", error)
+ val errorMessage = new StringWriter
+ error.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Encountered error polling message queue. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
// just keep going on the stream
pollingStream()
@@ -137,7 +142,7 @@ object CommandHandler {
_.evalMap[IO, Any] { message =>
message.command match {
case sync: ZoneChange
- if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.Create =>
+ if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.AutomatedSync || sync.changeType == ZoneChangeType.Create =>
logger.info(s"Updating visibility timeout for zone change; changeId=${sync.id}")
mq.changeMessageTimeout(message, 1.hour)
@@ -158,7 +163,7 @@ object CommandHandler {
_.evalMap[IO, MessageOutcome] { message =>
message.command match {
case sync: ZoneChange
- if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.Create =>
+ if sync.changeType == ZoneChangeType.Sync || sync.changeType == ZoneChangeType.AutomatedSync || sync.changeType == ZoneChangeType.Create =>
outcomeOf(message)(zoneSyncProcessor(sync))
case zoneChange: ZoneChange =>
@@ -182,7 +187,9 @@ object CommandHandler {
.attempt
.map {
case Left(e) =>
- logger.warn(s"Failed processing message need to retry; $message", e)
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.warn(s"Failed processing message need to retry; $message. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
RetryMessage(message)
case Right(ok) => ok
}
diff --git a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsBackend.scala b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsBackend.scala
index afcc77054..8619dbb26 100644
--- a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsBackend.scala
+++ b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsBackend.scala
@@ -29,6 +29,7 @@ import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record.{RecordSet, RecordSetChange, RecordSetChangeType, RecordType}
import vinyldns.core.domain.zone.{Algorithm, Zone, ZoneConnection}
+import java.io.{PrintWriter, StringWriter}
import scala.collection.JavaConverters._
object DnsProtocol {
@@ -165,6 +166,7 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
val dnsName = recordDnsName(name, zoneName)
logger.info(s"Querying for dns dnsRecordName='${dnsName.toString}'; recordType='$typ'")
val lookup = new DNS.Lookup(dnsName, toDnsRecordType(typ))
+
lookup.setResolver(resolver)
lookup.setSearchPath(List(Name.empty).asJava)
lookup.setCache(null)
@@ -213,8 +215,29 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
resp <- toDnsResponse(resp)
} yield resp
+ val message =
+ for {
+ str <- Either.catchNonFatal(s"DNS Resolver: ${resolver.toString}, " +
+ s"Resolver Address=${resolver.getAddress.getAddress}, Resolver Host=${resolver.getAddress.getHostName}, " +
+ s"Resolver Port=${resolver.getPort}, Timeout=${resolver.getTimeout.toString}"
+ )
+ } yield str
+
+ val resolver_debug_message = message match {
+ case Right(value) => value
+ case Left(_) => s"DNS Resolver: ${resolver.toString}"
+ }
+
+ val receivedResponse = result match {
+ case Right(value) => value.toString.replaceAll("\n",";").replaceAll("\t"," ")
+ case Left(e) =>
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")
+ }
+
logger.info(
- s"DnsConnection.send - Sending DNS Message ${obscuredDnsMessage(msg).toString}\n...received response $result"
+ s"DnsConnection.send - Sending DNS Message ${obscuredDnsMessage(msg).toString.replaceAll("\n",";").replaceAll("\t"," ")}. Received response: $receivedResponse. DNS Resolver Info: $resolver_debug_message"
)
result
@@ -234,10 +257,10 @@ class DnsBackend(val id: String, val resolver: DNS.SimpleResolver, val xfrInfo:
// so if we can parse the error into an rcode, then we need to handle it properly; otherwise, we can try again
// The DNS.Rcode.value function will return -1 if the error cannot be parsed into an integer
if (DNS.Rcode.value(query.error) >= 0) {
- logger.info(s"Received TRY_AGAIN from DNS lookup; converting error: ${query.error}")
+ logger.warn(s"Received TRY_AGAIN from DNS lookup; converting error: ${query.error.replaceAll("\n",";")}")
fromDnsRcodeToError(DNS.Rcode.value(query.error), query.error)
} else {
- logger.warn(s"Unparseable error code returned from DNS: ${query.error}")
+ logger.warn(s"Unparseable error code returned from DNS: ${query.error.replaceAll("\n",";")}")
Left(TryAgain(query.error))
}
@@ -293,7 +316,7 @@ object DnsBackend {
new DNS.TSIG(
parseAlgorithm(conn.algorithm),
decryptedConnection.keyName,
- decryptedConnection.key
+ decryptedConnection.key.value
)
}
diff --git a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala
index b89d00f04..3c9d3949e 100644
--- a/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala
+++ b/modules/api/src/main/scala/vinyldns/api/backend/dns/DnsConversions.scala
@@ -205,7 +205,8 @@ trait DnsConversions {
ttl = r.getTTL,
status = RecordSetStatus.Active,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
- records = f(r)
+ records = f(r),
+ recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
)
// if we do not know the record type, then we cannot parse the records, but we should be able to get everything else
@@ -217,7 +218,8 @@ trait DnsConversions {
ttl = r.getTTL,
status = RecordSetStatus.Active,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS),
- records = Nil
+ records = Nil,
+ recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
)
def fromARecord(r: DNS.ARecord, zoneName: DNS.Name, zoneId: String): RecordSet =
diff --git a/modules/api/src/main/scala/vinyldns/api/config/HighValueDomainConfig.scala b/modules/api/src/main/scala/vinyldns/api/config/HighValueDomainConfig.scala
index ab0429a51..b351ba26d 100644
--- a/modules/api/src/main/scala/vinyldns/api/config/HighValueDomainConfig.scala
+++ b/modules/api/src/main/scala/vinyldns/api/config/HighValueDomainConfig.scala
@@ -31,6 +31,6 @@ object HighValueDomainConfig {
"ip-list"
) {
case (regexList, ipList) =>
- HighValueDomainConfig(toCaseIgnoredRegexList(regexList), ipList.flatMap(IpAddress(_)))
+ HighValueDomainConfig(toCaseIgnoredRegexList(regexList), ipList.flatMap(IpAddress.fromString(_)))
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/config/ManualReviewConfig.scala b/modules/api/src/main/scala/vinyldns/api/config/ManualReviewConfig.scala
index 08b259f41..4b386b20f 100644
--- a/modules/api/src/main/scala/vinyldns/api/config/ManualReviewConfig.scala
+++ b/modules/api/src/main/scala/vinyldns/api/config/ManualReviewConfig.scala
@@ -41,7 +41,7 @@ object ManualReviewConfig {
ManualReviewConfig(
enabled,
toCaseIgnoredRegexList(domainsConfig.getStringList("domain-list").asScala.toList),
- domainsConfig.getStringList("ip-list").asScala.toList.flatMap(IpAddress(_)),
+ domainsConfig.getStringList("ip-list").asScala.toList.flatMap(IpAddress.fromString(_)),
domainsConfig.getStringList("zone-name-list").asScala.toSet
)
}
diff --git a/modules/api/src/main/scala/vinyldns/api/config/ServerConfig.scala b/modules/api/src/main/scala/vinyldns/api/config/ServerConfig.scala
index 30bae2b5c..6cbfb84d3 100644
--- a/modules/api/src/main/scala/vinyldns/api/config/ServerConfig.scala
+++ b/modules/api/src/main/scala/vinyldns/api/config/ServerConfig.scala
@@ -34,13 +34,14 @@ final case class ServerConfig(
keyName: String,
processingDisabled: Boolean,
useRecordSetCache: Boolean,
- loadTestData: Boolean
+ loadTestData: Boolean,
+ isZoneSyncScheduleAllowed: Boolean,
)
object ServerConfig {
import ZoneRecordValidations.toCaseIgnoredRegexList
- implicit val configReader: ConfigReader[ServerConfig] = ConfigReader.forProduct12[
+ implicit val configReader: ConfigReader[ServerConfig] = ConfigReader.forProduct13[
ServerConfig,
Int,
Int,
@@ -53,6 +54,7 @@ object ServerConfig {
Config,
Boolean,
Boolean,
+ Boolean,
Boolean
](
"health-check-timeout",
@@ -66,7 +68,8 @@ object ServerConfig {
"defaultZoneConnection",
"processing-disabled",
"use-recordset-cache",
- "load-test-data"
+ "load-test-data",
+ "is-zone-sync-schedule-allowed"
) {
case (
timeout,
@@ -80,7 +83,8 @@ object ServerConfig {
zoneConnConfig,
processingDisabled,
useRecordSetCache,
- loadTestData) =>
+ loadTestData,
+ isZoneSyncScheduleAllowed) =>
ServerConfig(
timeout,
ttl,
@@ -93,7 +97,8 @@ object ServerConfig {
zoneConnConfig.getString("keyName"),
processingDisabled,
useRecordSetCache,
- loadTestData
+ loadTestData,
+ isZoneSyncScheduleAllowed
)
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/config/ValidEmailConfig.scala b/modules/api/src/main/scala/vinyldns/api/config/ValidEmailConfig.scala
new file mode 100644
index 000000000..02997cd65
--- /dev/null
+++ b/modules/api/src/main/scala/vinyldns/api/config/ValidEmailConfig.scala
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018 Comcast Cable Communications Management, LLC
+ *
+ * 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 vinyldns.api.config
+
+import pureconfig.ConfigReader
+
+ case class ValidEmailConfig(
+ valid_domains : List[String],
+ number_of_dots : Int)
+object ValidEmailConfig {
+ implicit val configReader: ConfigReader[ValidEmailConfig] =
+ ConfigReader.forProduct2[ValidEmailConfig,List[String],Int](
+ "email-domains",
+ "number-of-dots"
+ )
+ {
+ case (
+ valid_domains,
+ number_of_dots,
+ ) =>
+ ValidEmailConfig(
+ valid_domains,
+ number_of_dots,
+
+ )
+ }
+
+}
diff --git a/modules/api/src/main/scala/vinyldns/api/config/VinylDNSConfig.scala b/modules/api/src/main/scala/vinyldns/api/config/VinylDNSConfig.scala
index dd41bb3a0..fb06539ec 100644
--- a/modules/api/src/main/scala/vinyldns/api/config/VinylDNSConfig.scala
+++ b/modules/api/src/main/scala/vinyldns/api/config/VinylDNSConfig.scala
@@ -38,6 +38,7 @@ import scala.reflect.ClassTag
final case class VinylDNSConfig(
serverConfig: ServerConfig,
limitsconfig: LimitsConfig,
+ validEmailConfig: ValidEmailConfig,
httpConfig: HttpConfig,
highValueDomainConfig: HighValueDomainConfig,
manualReviewConfig: ManualReviewConfig,
@@ -83,6 +84,7 @@ object VinylDNSConfig {
for {
config <- IO.delay(ConfigFactory.load())
limitsconfig <- loadIO[LimitsConfig](config, "vinyldns.api.limits") //Added Limitsconfig to fetch data from the reference.config and pass to LimitsConfig.config
+ validEmailConfig <- loadIO[ValidEmailConfig](config, path="vinyldns.valid-email-config")
serverConfig <- loadIO[ServerConfig](config, "vinyldns")
batchChangeConfig <- loadIO[BatchChangeConfig](config, "vinyldns")
backendConfigs <- loadIO[BackendConfigs](config, "vinyldns.backend")
@@ -103,6 +105,7 @@ object VinylDNSConfig {
} yield VinylDNSConfig(
serverConfig,
limitsconfig,
+ validEmailConfig,
httpConfig,
hvdConfig,
manualReviewConfig,
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala
index 5a12eb74b..786ec4c73 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/DomainValidations.scala
@@ -27,6 +27,7 @@ import scala.util.matching.Regex
Object to house common domain validations
*/
object DomainValidations {
+
val validReverseZoneFQDNRegex: Regex =
"""^(?:([0-9a-zA-Z\-\/_]{1,63}|[0-9a-zA-Z\-\/_]{1}[0-9a-zA-Z\-\/_]{0,61}[0-9a-zA-Z\-\/_]{1}|[*.]{2}[0-9a-zA-Z\-\/_]{0,60}[0-9a-zA-Z\-\/_]{1})\.)*$""".r
val validForwardZoneFQDNRegex: Regex =
@@ -58,16 +59,23 @@ object DomainValidations {
val TTL_MIN_LENGTH: Int = 30
val TXT_TEXT_MIN_LENGTH: Int = 1
val TXT_TEXT_MAX_LENGTH: Int = 64764
- val MX_PREFERENCE_MIN_VALUE: Int = 0
- val MX_PREFERENCE_MAX_VALUE: Int = 65535
+ val INTEGER_MIN_VALUE: Int = 0
+ val INTEGER_MAX_VALUE: Int = 65535
+
+ // Cname check - Cname should not be IP address
+ def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
+ validateIpv4Address(name.fqdn.dropRight(1)).isValid match {
+ case true => InvalidIPv4CName(name.toString).invalidNel
+ case false => validateIsReverseCname(name, isReverse)
+ }
def validateHostName(name: Fqdn): ValidatedNel[DomainValidationError, Fqdn] =
validateHostName(name.fqdn).map(_ => name)
- def validateCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
- validateCname(name.fqdn, isReverse).map(_ => name)
+ def validateIsReverseCname(name: Fqdn, isReverse: Boolean): ValidatedNel[DomainValidationError, Fqdn] =
+ validateIsReverseCname(name.fqdn, isReverse).map(_ => name)
- def validateCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = {
+ def validateIsReverseCname(name: String, isReverse: Boolean): ValidatedNel[DomainValidationError, String] = {
isReverse match {
case true =>
val checkRegex = validReverseZoneFQDNRegex
@@ -152,7 +160,15 @@ object DomainValidations {
def validateTxtTextLength(value: String): ValidatedNel[DomainValidationError, String] =
validateStringLength(value, Some(TXT_TEXT_MIN_LENGTH), TXT_TEXT_MAX_LENGTH)
- def validateMxPreference(pref: Int): ValidatedNel[DomainValidationError, Int] =
- if (pref >= MX_PREFERENCE_MIN_VALUE && pref <= MX_PREFERENCE_MAX_VALUE) pref.validNel
- else InvalidMxPreference(pref, MX_PREFERENCE_MIN_VALUE, MX_PREFERENCE_MAX_VALUE).invalidNel[Int]
+ def validateMX_NAPTR_SRVData(number: Int, recordDataType: String, recordType: String): ValidatedNel[DomainValidationError, Int] =
+ if (number >= INTEGER_MIN_VALUE && number <= INTEGER_MAX_VALUE) number.validNel
+ else InvalidMX_NAPTR_SRVData(number, INTEGER_MIN_VALUE, INTEGER_MAX_VALUE, recordDataType, recordType).invalidNel[Int]
+
+ def validateNaptrFlag(value: String): ValidatedNel[DomainValidationError, String] =
+ if (value == "U" || value == "S" || value == "A" || value == "P") value.validNel
+ else InvalidNaptrFlag(value).invalidNel[String]
+
+ def validateNaptrRegexp(value: String): ValidatedNel[DomainValidationError, String] =
+ if ((value.startsWith("!") && value.endsWith("!")) || value == "") value.validNel
+ else InvalidNaptrRegexp(value).invalidNel[String]
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/ReverseZoneHelpers.scala b/modules/api/src/main/scala/vinyldns/api/domain/ReverseZoneHelpers.scala
index a81fc6855..eb88c0fbf 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/ReverseZoneHelpers.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/ReverseZoneHelpers.scala
@@ -17,7 +17,7 @@
package vinyldns.api.domain
import cats.implicits._
-import com.aaronbedra.orchard.CIDR
+import com.comcast.ip4s.{Cidr, Ipv4Address, Ipv6Address}
import vinyldns.api.domain.zone.InvalidRequest
import vinyldns.core.domain.zone.Zone
import vinyldns.api.backend.dns.DnsConversions._
@@ -30,8 +30,9 @@ object ReverseZoneHelpers {
if (zone.isIPv4) {
recordsetIsWithinCidrMaskIpv4(mask: String, zone: Zone, recordName: String)
} else {
- val ipAddr = convertPTRtoIPv6(zone, recordName)
- Try(CIDR.valueOf(mask).contains(ipAddr)).getOrElse(false)
+ val ipAddr = Ipv6Address.fromString(convertPTRtoIPv6(zone, recordName))
+ Try(Cidr(Cidr.fromString6(mask).get.address,Cidr.fromString6(mask).get.prefixBits).contains(ipAddr.get))
+ .getOrElse(false)
}
// NOTE: this will not work for zones with less than 3 octets
@@ -86,11 +87,12 @@ object ReverseZoneHelpers {
zone: Zone,
recordName: String
): Boolean = {
- val recordIpAddr = convertPTRtoIPv4(zone, recordName)
+
+ val recordIpAddr = Ipv4Address.fromString(convertPTRtoIPv4(zone, recordName))
Try {
// make sure mask contains 4 octets, expand if not
- val ipMaskOctets = CIDR.parseBlock(mask).head.split('.').toList
+ val ipMaskOctets = Cidr.fromString4(mask).get.address.toString.split('.').toList
val fullIp = ipMaskOctets.length match {
case 1 => (ipMaskOctets ++ List("0", "0", "0")).mkString(".")
@@ -99,9 +101,8 @@ object ReverseZoneHelpers {
case 4 => ipMaskOctets.mkString(".")
}
- val updatedMask = fullIp + "/" + CIDR.valueOf(mask).getMask
-
- CIDR.valueOf(updatedMask).contains(recordIpAddr)
+ val updatedMask = Cidr(recordIpAddr.get,Cidr.fromString4(mask).get.prefixBits)
+ updatedMask.contains(Ipv4Address.fromString(fullIp).get)
}.getOrElse(false)
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidations.scala
index 845b2c034..2fc553051 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidations.scala
@@ -38,6 +38,13 @@ class AccessValidations(
.isGroupMember(zone.adminGroupId) || userHasAclRules(auth, zone)
)
+ def canSeeZoneChange(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit] =
+ ensuring(
+ NotAuthorizedError(s"User ${auth.signedInUser.userName} cannot access zone '${zone.name}' changes")
+ )(
+ auth.isSystemAdmin || zone.shared || auth.isGroupMember(zone.adminGroupId)
+ )
+
def canChangeZone(
auth: AuthPrincipal,
zoneName: String,
@@ -73,6 +80,7 @@ class AccessValidations(
recordType: RecordType,
zone: Zone,
recordOwnerGroupId: Option[String],
+ superUserCanUpdateOwnerGroup: Boolean = false,
newRecordData: List[RecordData] = List.empty
): Either[Throwable, Unit] = {
val accessLevel =
@@ -82,7 +90,7 @@ class AccessValidations(
s"User ${auth.signedInUser.userName} does not have access to update " +
s"$recordName.${zone.name}"
)
- )(accessLevel == AccessLevel.Delete || accessLevel == AccessLevel.Write)
+ )(accessLevel == AccessLevel.Delete || accessLevel == AccessLevel.Write || superUserCanUpdateOwnerGroup)
}
def canDeleteRecordSet(
@@ -222,7 +230,9 @@ class AccessValidations(
AccessLevel.Delete
case support if support.isSystemAdmin =>
val aclAccess = getAccessFromAcl(auth, recordName, recordType, zone)
- if (aclAccess == AccessLevel.NoAccess) AccessLevel.Read else aclAccess
+ if (aclAccess == AccessLevel.NoAccess)
+ AccessLevel.Read
+ else aclAccess
case globalAclUser
if globalAcls.isAuthorized(globalAclUser, recordName, recordType, zone, recordData) =>
AccessLevel.Delete
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidationsAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidationsAlgebra.scala
index 59d6c300a..b1b5c2edb 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidationsAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/access/AccessValidationsAlgebra.scala
@@ -27,6 +27,8 @@ trait AccessValidationsAlgebra {
def canSeeZone(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit]
+ def canSeeZoneChange(auth: AuthPrincipal, zone: Zone): Either[Throwable, Unit]
+
def canChangeZone(
auth: AuthPrincipal,
zoneName: String,
@@ -47,6 +49,7 @@ trait AccessValidationsAlgebra {
recordType: RecordType,
zone: Zone,
recordOwnerGroupId: Option[String],
+ superUserCanUpdateOwnerGroup: Boolean = false,
newRecordData: List[RecordData] = List.empty
): Either[Throwable, Unit]
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala
index e4fd32b22..1f112a6a2 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeConverter.scala
@@ -34,8 +34,6 @@ import vinyldns.core.queue.MessageQueue
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
extends BatchChangeConverterAlgebra {
- private val notExistCompletedMessage: String = "This record does not exist." +
- "No further action is required."
private val failedMessage: String = "Error queueing RecordSetChange for processing"
private val logger = LoggerFactory.getLogger(classOf[BatchChangeConverter])
@@ -125,7 +123,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
change match {
case _: SingleDeleteRRSetChange if change.recordSetId.isEmpty =>
// Mark as Complete since we don't want to throw it as an error
- change.withDoesNotExistMessage(notExistCompletedMessage)
+ change.withDoesNotExistMessage
case _ =>
// Failure here means there was a message queue issue for this change
change.withFailureMessage(failedMessage)
@@ -138,9 +136,10 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
// Update if Single change is Failed or if a record that does not exist is deleted
val failedAndNotExistsChanges = batchChange.changes.collect {
- case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(notExistCompletedMessage) => change
+ case change if change.status == SingleChangeStatus.Failed => change
}
- batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
+ val storeChanges = batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
+ storeChanges
}.toBatchResult
def createRecordSetChangesForBatch(
@@ -215,7 +214,7 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
}
}
- // New record set for add/update or single delete
+ // New record set for add/update/full deletes
lazy val newRecordSet = {
val firstAddChange = singleChangeNel.collect {
case sac: SingleAddChange => sac
@@ -237,6 +236,33 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
proposedRecordData.toList,
+ ownerGroupId = setOwnerGroupId,
+ recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.None))
+ )
+ }
+ }
+
+ // New record set for single delete which exists in dns backend but not in vinyl
+ lazy val newDeleteRecordSet = {
+ val firstDeleteChange = singleChangeNel.collect {
+ case sad: SingleDeleteRRSetChange => sad
+ }.headOption
+
+ val newTtlRecordNameTuple = firstDeleteChange
+ .map(del => del.recordName)
+ .orElse(existingRecordSet.map(rs => Some(rs.name)))
+
+ newTtlRecordNameTuple.collect{
+ case Some(recordName) =>
+ RecordSet(
+ zone.id,
+ recordName,
+ recordType,
+ 7200L,
+ RecordSetStatus.Pending,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ None,
+ proposedRecordData.toList,
ownerGroupId = setOwnerGroupId
)
}
@@ -257,7 +283,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
existingRs <- existingRecordSet
newRs <- newRecordSet
} yield RecordSetChangeGenerator.forUpdate(existingRs, newRs, zone, userId, singleChangeIds)
- case _ => None // This case should never happen
+ case OutOfSync =>
+ newDeleteRecordSet.map { newDelRs =>
+ RecordSetChangeGenerator.forOutOfSync(newDelRs, zone, userId, singleChangeIds)
+ }
+ case _ =>
+ None // This case should never happen
}
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeProtocol.scala
index cef50fbd1..6b3b98364 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeProtocol.scala
@@ -44,12 +44,14 @@ object BatchChangeInput {
sealed trait ChangeInput {
val inputName: String
val typ: RecordType
+ val systemMessage: Option[String]
def asNewStoredChange(errors: NonEmptyList[DomainValidationError], defaultTtl: Long): SingleChange
}
final case class AddChangeInput(
inputName: String,
typ: RecordType,
+ systemMessage: Option[String],
ttl: Option[Long],
record: RecordData
) extends ChangeInput {
@@ -68,7 +70,7 @@ final case class AddChangeInput(
knownTtl,
record,
SingleChangeStatus.NeedsReview,
- None,
+ systemMessage,
None,
None,
errors.toList.map(SingleChangeError(_))
@@ -79,6 +81,7 @@ final case class AddChangeInput(
final case class DeleteRRSetChangeInput(
inputName: String,
typ: RecordType,
+ systemMessage: Option[String],
record: Option[RecordData]
) extends ChangeInput {
def asNewStoredChange(
@@ -93,7 +96,7 @@ final case class DeleteRRSetChangeInput(
typ,
record,
SingleChangeStatus.NeedsReview,
- None,
+ systemMessage,
None,
None,
errors.toList.map(SingleChangeError(_))
@@ -104,6 +107,7 @@ object AddChangeInput {
def apply(
inputName: String,
typ: RecordType,
+ systemMessage: Option[String],
ttl: Option[Long],
record: RecordData
): AddChangeInput = {
@@ -111,28 +115,29 @@ object AddChangeInput {
case PTR => inputName
case _ => ensureTrailingDot(inputName)
}
- new AddChangeInput(transformName, typ, ttl, record)
+ new AddChangeInput(transformName, typ, systemMessage, ttl, record)
}
def apply(sc: SingleAddChange): AddChangeInput =
- AddChangeInput(sc.inputName, sc.typ, Some(sc.ttl), sc.recordData)
+ AddChangeInput(sc.inputName, sc.typ, sc.systemMessage, Some(sc.ttl), sc.recordData)
}
object DeleteRRSetChangeInput {
def apply(
inputName: String,
typ: RecordType,
+ systemMessage: Option[String],
record: Option[RecordData] = None
): DeleteRRSetChangeInput = {
val transformName = typ match {
case PTR => inputName
case _ => ensureTrailingDot(inputName)
}
- new DeleteRRSetChangeInput(transformName, typ, record)
+ new DeleteRRSetChangeInput(transformName, typ, systemMessage, record)
}
def apply(sc: SingleDeleteRRSetChange): DeleteRRSetChangeInput =
- DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.recordData)
+ DeleteRRSetChangeInput(sc.inputName, sc.typ, sc.systemMessage, sc.recordData)
}
object ChangeInputType extends Enumeration {
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeService.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeService.scala
index a6746d640..d0a364b5e 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeService.scala
@@ -20,6 +20,7 @@ import cats.data.Validated.{Invalid, Valid}
import cats.data._
import cats.effect._
import cats.implicits._
+
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.slf4j.{Logger, LoggerFactory}
@@ -33,6 +34,7 @@ import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
import vinyldns.core.domain.batch._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus._
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
import vinyldns.core.domain.{CnameAtZoneApexError, SingleChangeError, UserIsNotAuthorizedError, ZoneDiscoveryError}
import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
import vinyldns.core.domain.record.RecordType._
@@ -340,7 +342,7 @@ class BatchChangeService(
): ValidatedBatch[ChangeForValidation] =
changes.mapValid { change =>
change.typ match {
- case A | AAAA | CNAME | MX | TXT => forwardZoneDiscovery(change, zoneMap)
+ case A | AAAA | CNAME | MX | TXT | NS | NAPTR | SRV => forwardZoneDiscovery(change, zoneMap)
case PTR if validateIpv4Address(change.inputName).isValid =>
ptrIpv4ZoneDiscovery(change, zoneMap)
case PTR if validateIpv6Address(change.inputName).isValid =>
@@ -470,14 +472,15 @@ class BatchChangeService(
val hardErrorsPresent = allErrors.exists(_.isFatal)
val noErrors = allErrors.isEmpty
val isScheduled = batchChangeInput.scheduledTime.isDefined && this.scheduledChangesEnabled
+ val isNSRecordsPresent = batchChangeInput.changes.exists(_.typ == NS)
if (hardErrorsPresent) {
// Always error out
errorResponse
- } else if (noErrors && !isScheduled) {
+ } else if (noErrors && !isScheduled && !isNSRecordsPresent) {
// There are no errors and this is not scheduled, so process immediately
processNowResponse
- } else if (this.manualReviewEnabled && allowManualReview) {
+ } else if (this.manualReviewEnabled && allowManualReview || isNSRecordsPresent) {
if ((noErrors && isScheduled) || batchChangeInput.ownerGroupId.isDefined) {
// There are no errors and this is scheduled
// or we have soft errors and owner group is defined
@@ -579,15 +582,22 @@ class BatchChangeService(
def listBatchChangeSummaries(
auth: AuthPrincipal,
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int] = None,
maxItems: Int = 100,
ignoreAccess: Boolean = false,
+ batchStatus: Option[BatchChangeStatus] = None,
approvalStatus: Option[BatchChangeApprovalStatus] = None
): BatchResult[BatchChangeSummaryList] = {
val userId = if (ignoreAccess && auth.isSystemAdmin) None else Some(auth.userId)
+ val submitterUserName = if(userName.isDefined && userName.get.isEmpty) None else userName
+ val startDateTime = if(dateTimeStartRange.isDefined && dateTimeStartRange.get.isEmpty) None else dateTimeStartRange
+ val endDateTime = if(dateTimeEndRange.isDefined && dateTimeEndRange.get.isEmpty) None else dateTimeEndRange
for {
listResults <- batchChangeRepo
- .getBatchChangeSummaries(userId, startFrom, maxItems, approvalStatus)
+ .getBatchChangeSummaries(userId, submitterUserName, startDateTime, endDateTime, startFrom, maxItems, batchStatus, approvalStatus)
.toBatchResult
rsOwnerGroupIds = listResults.batchChanges.flatMap(_.ownerGroupId).toSet
rsOwnerGroups <- groupRepository.getGroups(rsOwnerGroupIds).toBatchResult
@@ -604,7 +614,10 @@ class BatchChangeService(
listWithGroupNames = listResults.copy(
batchChanges = summariesWithReviewerUserNames,
ignoreAccess = ignoreAccess,
- approvalStatus = approvalStatus
+ approvalStatus = approvalStatus,
+ userName = userName,
+ dateTimeStartRange = dateTimeStartRange,
+ dateTimeEndRange = dateTimeEndRange
)
} yield listWithGroupNames
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeServiceAlgebra.scala
index aa66318a3..36f2f513e 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeServiceAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeServiceAlgebra.scala
@@ -19,6 +19,7 @@ package vinyldns.api.domain.batch
import vinyldns.api.domain.batch.BatchChangeInterfaces.BatchResult
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
import vinyldns.core.domain.batch.{BatchChange, BatchChangeInfo, BatchChangeSummaryList}
// $COVERAGE-OFF$
@@ -33,9 +34,13 @@ trait BatchChangeServiceAlgebra {
def listBatchChangeSummaries(
auth: AuthPrincipal,
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int],
maxItems: Int,
ignoreAccess: Boolean,
+ batchStatus: Option[BatchChangeStatus],
approvalStatus: Option[BatchChangeApprovalStatus]
): BatchResult[BatchChangeSummaryList]
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala
index d9e0bce97..2f38050dd 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchChangeValidations.scala
@@ -16,7 +16,6 @@
package vinyldns.api.domain.batch
-import java.net.InetAddress
import java.time.Instant
import java.time.temporal.ChronoUnit
import cats.data._
@@ -27,11 +26,16 @@ import vinyldns.api.domain.access.AccessValidationsAlgebra
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.api.domain.batch.BatchChangeInterfaces._
import vinyldns.api.domain.batch.BatchTransformations._
+import vinyldns.api.domain.zone.ZoneRecordValidations.isStringInRegexList
import vinyldns.api.domain.zone.ZoneRecordValidations
+import vinyldns.core.Messages.{nonExistentRecordDataDeleteMessage, nonExistentRecordDeleteMessage}
+import vinyldns.core.domain.DomainHelpers.omitTrailingDot
import vinyldns.core.domain.record._
import vinyldns.core.domain._
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, OwnerType, RecordKey, RecordKeyData}
import vinyldns.core.domain.membership.Group
+import vinyldns.core.domain.zone.Zone
+import scala.util.matching.Regex
trait BatchChangeValidationsAlgebra {
@@ -81,7 +85,8 @@ class BatchChangeValidations(
highValueDomainConfig: HighValueDomainConfig,
manualReviewConfig: ManualReviewConfig,
batchChangeConfig: BatchChangeConfig,
- scheduledChangesConfig: ScheduledChangesConfig
+ scheduledChangesConfig: ScheduledChangesConfig,
+ approvedNameServers: List[Regex]
) extends BatchChangeValidationsAlgebra {
import RecordType._
@@ -214,8 +219,8 @@ class BatchChangeValidations(
}
def validateDeleteRRSetChangeInput(
- deleteRRSetChangeInput: DeleteRRSetChangeInput,
- isApproved: Boolean
+ deleteRRSetChangeInput: DeleteRRSetChangeInput,
+ isApproved: Boolean
): SingleValidation[Unit] = {
val validRecord = deleteRRSetChangeInput.record match {
case Some(recordData) => validateRecordData(recordData, deleteRRSetChangeInput)
@@ -241,14 +246,17 @@ class BatchChangeValidations(
case ptr: PTRData => validateHostName(ptr.ptrdname).asUnit
case txt: TXTData => validateTxtTextLength(txt.text).asUnit
case mx: MXData =>
- validateMxPreference(mx.preference).asUnit |+| validateHostName(mx.exchange).asUnit
+ validateMX_NAPTR_SRVData(mx.preference, "preference", "MX").asUnit |+| validateHostName(mx.exchange).asUnit
+ case ns: NSData => validateHostName(ns.nsdname).asUnit
+ case naptr: NAPTRData => validateMX_NAPTR_SRVData(naptr.preference, "preference", "NAPTR").asUnit |+| validateMX_NAPTR_SRVData(naptr.order, "order", "NAPTR").asUnit |+| validateHostName(naptr.replacement).asUnit |+| validateNaptrFlag(naptr.flags).asUnit |+| validateNaptrRegexp(naptr.regexp).asUnit
+ case srv: SRVData => validateMX_NAPTR_SRVData(srv.priority, "priority", "SRV").asUnit |+| validateMX_NAPTR_SRVData(srv.port, "port", "SRV").asUnit |+| validateMX_NAPTR_SRVData(srv.weight, "weight", "SRV").asUnit |+| validateHostName(srv.target).asUnit
case other =>
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel[Unit]
}
def validateInputName(change: ChangeInput, isApproved: Boolean): SingleValidation[Unit] = {
val typedChecks = change.typ match {
- case A | AAAA | MX =>
+ case A | AAAA | MX | NS | NAPTR | SRV =>
validateHostName(change.inputName).asUnit |+| notInReverseZone(change)
case CNAME | TXT =>
validateHostName(change.inputName).asUnit
@@ -307,35 +315,34 @@ class BatchChangeValidations(
else
().validNel
- def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean =
- existingRecordSetData.exists { rd =>
- (rd, recordData) match {
- case (AAAAData(rdAddress), AAAAData(proposedAddress)) =>
- InetAddress.getByName(proposedAddress).getHostName == InetAddress
- .getByName(rdAddress)
- .getHostName
- case _ => rd == recordData
- }
+ def matchRecordData(existingRecordSetData: List[RecordData], recordData: RecordData): Boolean = {
+ existingRecordSetData.par.exists { rd =>
+ rd == recordData
}
+ }
def ensureRecordExists(
- change: ChangeForValidation,
- groupedChanges: ChangeForValidationMap
- ): SingleValidation[Unit] =
+ change: ChangeForValidation,
+ groupedChanges: ChangeForValidationMap
+ ): Boolean = {
change match {
// For DeleteRecord inputs, need to verify that the record data actually exists
- case DeleteRRSetChangeForValidation(
- _,
- _,
- DeleteRRSetChangeInput(inputName, _, Some(recordData))
- )
- if !groupedChanges
- .getExistingRecordSet(change.recordKey)
- .exists(rs => matchRecordData(rs.records, recordData)) =>
- DeleteRecordDataDoesNotExist(inputName, recordData).invalidNel
+ case DeleteRRSetChangeForValidation(_, _, DeleteRRSetChangeInput(_, _, _, Some(recordData)))
+ if !groupedChanges
+ .getExistingRecordSet(change.recordKey)
+ .exists(rs => matchRecordData(rs.records, recordData)) =>
+ false
case _ =>
- ().validNel
+ true
}
+ }
+
+ def updateSystemMessage(changeInput: ChangeInput, systemMessage: String): ChangeInput = {
+ changeInput match {
+ case dci: DeleteRRSetChangeInput => dci.copy(systemMessage = Some(systemMessage))
+ case _ => changeInput
+ }
+ }
def validateDeleteWithContext(
change: ChangeForValidation,
@@ -344,15 +351,34 @@ class BatchChangeValidations(
isApproved: Boolean
): SingleValidation[ChangeForValidation] = {
- val validations =
- groupedChanges.getExistingRecordSet(change.recordKey) match {
- case Some(rs) =>
- userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
- zoneDoesNotRequireManualReview(change, isApproved) |+|
- ensureRecordExists(change, groupedChanges)
- case None => RecordDoesNotExist(change.inputChange.inputName).validNel
- }
- validations.map(_ => change)
+ val recordData = change match {
+ case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
+ case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
+ }
+
+ val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
+ val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
+
+ // Perform the system message update based on the condition
+ val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
+ val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
+ change.withUpdatedInputChange(updatedChangeInput)
+ } else if (!ensureRecordExists(change, groupedChanges)) {
+ val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
+ change.withUpdatedInputChange(updatedChangeInput)
+ } else {
+ change
+ }
+
+ val validations = groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
+ case Some(rs) =>
+ userCanDeleteRecordSet(updatedChange, auth, rs.ownerGroupId, rs.records) |+|
+ zoneDoesNotRequireManualReview(updatedChange, isApproved)
+ case None =>
+ if (isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
+ }
+
+ validations.map(_ => updatedChange)
}
def validateAddUpdateWithContext(
@@ -381,7 +407,7 @@ class BatchChangeValidations(
) |+|
zoneDoesNotRequireManualReview(change, isApproved)
case None =>
- RecordDoesNotExist(change.inputChange.inputName).invalidNel
+ InvalidUpdateRequest(change.inputChange.inputName).invalidNel
}
}
@@ -396,18 +422,38 @@ class BatchChangeValidations(
auth: AuthPrincipal,
isApproved: Boolean
): SingleValidation[ChangeForValidation] = {
+
+ // To handle add and delete for the record with same record data is present in the batch
+ val recordData = change match {
+ case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
+ case DeleteRRSetChangeForValidation(_, _, inputChange) => inputChange.record.map(_.toString).getOrElse("")
+ }
+
+ val addInBatch = groupedChanges.getProposedAdds(change.recordKey)
+ val isSameRecordUpdateInBatch = recordData.nonEmpty && addInBatch.contains(RecordData.fromString(recordData, change.inputChange.typ).get)
+
+ // Perform the system message update based on the condition
+ val updatedChange = if (groupedChanges.getExistingRecordSet(change.recordKey).isEmpty && !isSameRecordUpdateInBatch) {
+ val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDeleteMessage)
+ change.withUpdatedInputChange(updatedChangeInput)
+ } else if (!ensureRecordExists(change, groupedChanges)) {
+ val updatedChangeInput = updateSystemMessage(change.inputChange, nonExistentRecordDataDeleteMessage)
+ change.withUpdatedInputChange(updatedChangeInput)
+ } else {
+ change
+ }
+
val validations =
- groupedChanges.getExistingRecordSet(change.recordKey) match {
+ groupedChanges.getExistingRecordSet(updatedChange.recordKey) match {
case Some(rs) =>
- val adds = groupedChanges.getProposedAdds(change.recordKey).toList
- userCanUpdateRecordSet(change, auth, rs.ownerGroupId, adds) |+|
- zoneDoesNotRequireManualReview(change, isApproved) |+|
- ensureRecordExists(change, groupedChanges)
+ val adds = groupedChanges.getProposedAdds(updatedChange.recordKey).toList
+ userCanUpdateRecordSet(updatedChange, auth, rs.ownerGroupId, adds) |+|
+ zoneDoesNotRequireManualReview(updatedChange, isApproved)
case None =>
- RecordDoesNotExist(change.inputChange.inputName).validNel
+ if(isSameRecordUpdateInBatch) InvalidUpdateRequest(updatedChange.inputChange.inputName).invalidNel else ().validNel
}
- validations.map(_ => change)
+ validations.map(_ => updatedChange)
}
def validateAddWithContext(
@@ -418,8 +464,10 @@ class BatchChangeValidations(
ownerGroupId: Option[String]
): SingleValidation[ChangeForValidation] = {
val typedValidations = change.inputChange.typ match {
- case A | AAAA | MX =>
+ case A | AAAA | MX | SRV | NAPTR =>
newRecordSetIsNotDotted(change)
+ case NS =>
+ newRecordSetIsNotDotted(change) |+| nsValidations(change.inputChange.record, change.recordName, change.zone, approvedNameServers)
case CNAME =>
cnameHasUniqueNameInBatch(change, groupedChanges) |+|
newRecordSetIsNotDotted(change)
@@ -429,8 +477,29 @@ class BatchChangeValidations(
InvalidBatchRecordType(other.toString, SupportedBatchChangeRecordTypes.get).invalidNel
}
+ // To handle add and delete for the record with same record data is present in the batch
+ val recordData = change match {
+ case AddChangeForValidation(_, _, inputChange, _, _) => inputChange.record.toString
+ }
+
+ val deletes = groupedChanges.getProposedDeletes(change.recordKey)
+ val isDeleteExists = deletes.nonEmpty
+ val isSameRecordUpdateInBatch = if(recordData.nonEmpty){
+ if(deletes.contains(RecordData.fromString(recordData, change.inputChange.typ).get)) true else false
+ } else false
+
+ val commonValidations: SingleValidation[Unit] = {
+ groupedChanges.getExistingRecordSet(change.recordKey) match {
+ case Some(_) =>
+ ().validNel
+ case None =>
+ if(isSameRecordUpdateInBatch) InvalidUpdateRequest(change.inputChange.inputName).invalidNel else ().validNel
+ }
+ }
+
val validations =
typedValidations |+|
+ commonValidations |+|
noIncompatibleRecordExists(change, groupedChanges) |+|
userCanAddRecordSet(change, auth) |+|
recordDoesNotExist(
@@ -440,7 +509,7 @@ class BatchChangeValidations(
change.inputChange.typ,
change.inputChange.record,
groupedChanges,
- isApproved
+ isDeleteExists
) |+|
ownerGroupProvidedIfNeeded(change, None, ownerGroupId) |+|
zoneDoesNotRequireManualReview(change, isApproved)
@@ -483,13 +552,14 @@ class BatchChangeValidations(
typ: RecordType,
recordData: RecordData,
groupedChanges: ChangeForValidationMap,
- isApproved: Boolean
+ isDeleteExist: Boolean
): SingleValidation[Unit] = {
val record = groupedChanges.getExistingRecordSetData(RecordKeyData(zoneId, recordName, typ, recordData))
if(record.isDefined) {
record.get.records.contains(recordData) match {
case true => ().validNel
- case false => RecordAlreadyExists(inputName, recordData, isApproved).invalidNel}
+ case false => if(isDeleteExist) ().validNel else RecordAlreadyExists(inputName).invalidNel
+ }
} else ().validNel
}
@@ -561,6 +631,7 @@ class BatchChangeValidations(
input.inputChange.typ,
input.zone,
ownerGroupId,
+ false,
addRecords
)
result
@@ -697,4 +768,46 @@ class BatchChangeValidations(
change.inputChange.inputName
)
}
+
+ def nsValidations(
+ newRecordSetData: RecordData,
+ newRecordSetName: String,
+ zone: Zone,
+ approvedNameServers: List[Regex]
+ ): SingleValidation[Unit] = {
+
+ isNotOrigin(
+ newRecordSetName,
+ zone,
+ s"Record with name $newRecordSetName is an NS record at apex and cannot be added"
+ )
+ containsApprovedNameServers(newRecordSetData, approvedNameServers)
+ }
+
+ def isNotOrigin(recordSet: String, zone: Zone, err: String): SingleValidation[Unit] =
+ if(!isOriginRecord(recordSet, omitTrailingDot(zone.name))) ().validNel else InvalidBatchRequest(err).invalidNel
+
+ def isOriginRecord(recordSetName: String, zoneName: String): Boolean =
+ recordSetName == "@" || omitTrailingDot(recordSetName) == omitTrailingDot(zoneName)
+
+ def containsApprovedNameServers(
+ nsRecordSet: RecordData,
+ approvedNameServers: List[Regex]
+ ): SingleValidation[Unit] = {
+ val nsData = nsRecordSet match {
+ case ns: NSData => ns
+ case _ => ??? // this would never be the case
+ }
+ isApprovedNameServer(approvedNameServers, nsData)
+ }
+
+ def isApprovedNameServer(
+ approvedServerList: List[Regex],
+ nsData: NSData
+ ): SingleValidation[Unit] =
+ if (isStringInRegexList(approvedServerList, nsData.nsdname.fqdn)) {
+ ().validNel
+ } else {
+ NotApprovedNSError(nsData.nsdname.fqdn).invalidNel
+ }
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchTransformations.scala b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchTransformations.scala
index 5afa5da02..002717e7b 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchTransformations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/batch/BatchTransformations.scala
@@ -16,7 +16,6 @@
package vinyldns.api.domain.batch
-import java.net.InetAddress
import java.util.UUID
import vinyldns.api.domain.ReverseZoneHelpers
@@ -24,13 +23,13 @@ import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType.LogicalChangeType
import vinyldns.api.backend.dns.DnsConversions.getIPv6FullReverseName
import vinyldns.core.domain.batch._
-import vinyldns.core.domain.record.{AAAAData, RecordData, RecordSet, RecordSetChange}
+import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.zone.Zone
import vinyldns.core.domain.record.RecordType.RecordType
object SupportedBatchChangeRecordTypes {
- val supportedTypes = Set(A, AAAA, CNAME, PTR, TXT, MX)
+ val supportedTypes = Set(A, AAAA, CNAME, PTR, TXT, MX, NS, SRV, NAPTR)
def get: Set[RecordType] = supportedTypes
}
@@ -82,6 +81,7 @@ object BatchTransformations {
val recordKey = RecordKey(zone.id, recordName, inputChange.typ)
def asStoredChange(changeId: Option[String] = None): SingleChange
def isAddChangeForValidation: Boolean
+ def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation
}
object ChangeForValidation {
@@ -118,7 +118,7 @@ object BatchTransformations {
ttl,
inputChange.record,
SingleChangeStatus.Pending,
- None,
+ inputChange.systemMessage,
None,
None,
List.empty,
@@ -127,6 +127,10 @@ object BatchTransformations {
}
def isAddChangeForValidation: Boolean = true
+
+ def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
+ this.copy(inputChange = inputChange.asInstanceOf[AddChangeInput])
+ }
}
final case class DeleteRRSetChangeForValidation(
@@ -143,7 +147,7 @@ object BatchTransformations {
inputChange.typ,
inputChange.record,
SingleChangeStatus.Pending,
- None,
+ inputChange.systemMessage,
None,
None,
List.empty,
@@ -151,6 +155,10 @@ object BatchTransformations {
)
def isAddChangeForValidation: Boolean = false
+
+ def withUpdatedInputChange(inputChange: ChangeInput): ChangeForValidation = {
+ this.copy(inputChange = inputChange.asInstanceOf[DeleteRRSetChangeInput])
+ }
}
final case class BatchConversionOutput(
@@ -180,6 +188,9 @@ object BatchTransformations {
def getProposedAdds(recordKey: RecordKey): Set[RecordData] =
innerMap.get(recordKey).map(_.proposedAdds).toSet.flatten
+ def getProposedDeletes(recordKey: RecordKey): Set[RecordData] =
+ innerMap.get(recordKey).map(_.proposedDeletes).toSet.flatten
+
// The new, net record data factoring in existing records, deletes and adds
// If record is not edited in batch, will fallback to look up record in existing
// records
@@ -194,13 +205,6 @@ object BatchTransformations {
}
object ValidationChanges {
- def matchRecordData(existingRecord: RecordData, recordData: String): Boolean =
- existingRecord match {
- case AAAAData(address) =>
- InetAddress.getByName(address).getHostName ==
- InetAddress.getByName(recordData).getHostName
- case _ => false
- }
def apply(
changes: List[ChangeForValidation],
@@ -220,16 +224,11 @@ object BatchTransformations {
case DeleteRRSetChangeForValidation(
_,
_,
- DeleteRRSetChangeInput(_, AAAA, Some(AAAAData(address)))
- ) =>
- existingRecords.filter(r => matchRecordData(r, address))
- case DeleteRRSetChangeForValidation(
- _,
- _,
- DeleteRRSetChangeInput(_, _, Some(recordData))
+ DeleteRRSetChangeInput(_, _, _, Some(recordData))
) =>
Set(recordData)
- case _: DeleteRRSetChangeForValidation => existingRecords
+ case _: DeleteRRSetChangeForValidation =>
+ existingRecords
}
.toSet
.flatten
@@ -237,26 +236,45 @@ object BatchTransformations {
// New proposed record data (assuming all validations pass)
val proposedRecordData = existingRecords -- deleteChangeSet ++ addChangeRecordDataSet
- // Note: "Update" where an Add and DeleteRecordSet is provided for a DNS record that does not exist will be
- // treated as a logical Add since the delete validation will fail (on record does not exist)
val logicalChangeType = (addChangeRecordDataSet.nonEmpty, deleteChangeSet.nonEmpty) match {
- case (true, true) => LogicalChangeType.Update
- case (true, false) => LogicalChangeType.Add
- case (false, true) =>
- if ((existingRecords -- deleteChangeSet).isEmpty) {
- LogicalChangeType.FullDelete
+ case (true, true) =>
+ if (existingRecords.isEmpty) {
+ // Note: "Add" where an Add and DeleteRecordSet is provided for a DNS record that does not exist.
+ // Adds the record if it doesn't exist and ignores the delete.
+ LogicalChangeType.Add
} else {
+ // Note: "Update" where an Add and DeleteRecordSet is provided for a DNS record that exist, but record data for DeleteRecordSet does not exist.
+ // Updates the record and ignores the delete.
LogicalChangeType.Update
}
- case (false, false) => LogicalChangeType.NotEditedInBatch
+ case (true, false) => LogicalChangeType.Add
+ case (false, true) =>
+ if (existingRecords == deleteChangeSet) {
+ LogicalChangeType.FullDelete
+ } else if (existingRecords.nonEmpty) {
+ LogicalChangeType.Update
+ } else {
+ LogicalChangeType.OutOfSync
+ }
+ case (false, false) =>
+ if(changes.exists {
+ case _: DeleteRRSetChangeForValidation => true
+ case _ => false
+ }
+ ){
+ LogicalChangeType.OutOfSync
+ } else {
+ LogicalChangeType.NotEditedInBatch
+ }
}
- new ValidationChanges(addChangeRecordDataSet, proposedRecordData, logicalChangeType)
+ new ValidationChanges(addChangeRecordDataSet, deleteChangeSet, proposedRecordData, logicalChangeType)
}
}
final case class ValidationChanges(
proposedAdds: Set[RecordData],
+ proposedDeletes: Set[RecordData],
proposedRecordData: Set[RecordData],
logicalChangeType: LogicalChangeType
)
@@ -269,6 +287,6 @@ object BatchTransformations {
object LogicalChangeType extends Enumeration {
type LogicalChangeType = Value
- val Add, FullDelete, Update, NotEditedInBatch = Value
+ val Add, FullDelete, Update, NotEditedInBatch, OutOfSync = Value
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
index 5f7951085..8253b2f93 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipProtocol.scala
@@ -104,14 +104,16 @@ object UserInfo {
case class UserResponseInfo(
id: String,
- userName: Option[String] = None
+ userName: Option[String] = None,
+ groupId: Set[String] = Set.empty
)
object UserResponseInfo {
- def apply(user: User): UserResponseInfo =
+ def apply(user: User , group: Group): UserResponseInfo =
UserResponseInfo(
id = user.id,
- userName = Some(user.userName)
+ userName = Some(user.userName),
+ groupId = Set(group.id)
)
}
@@ -158,8 +160,8 @@ final case class ListAdminsResponse(admins: Seq[UserInfo])
final case class ListGroupChangesResponse(
changes: Seq[GroupChangeInfo],
- startFrom: Option[String] = None,
- nextId: Option[String] = None,
+ startFrom: Option[Int] = None,
+ nextId: Option[Int] = None,
maxItems: Int
)
@@ -178,6 +180,8 @@ final case class GroupAlreadyExistsError(msg: String) extends Throwable(msg)
final case class GroupValidationError(msg: String) extends Throwable(msg)
+final case class EmailValidationError(msg: String) extends Throwable(msg)
+
final case class UserNotFoundError(msg: String) extends Throwable(msg)
final case class InvalidGroupError(msg: String) extends Throwable(msg)
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
index 08e7cc240..b77c22943 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipService.scala
@@ -20,6 +20,7 @@ import cats.effect.IO
import cats.implicits._
import scalikejdbc.DB
import vinyldns.api.Interfaces._
+import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.repository.ApiDataAccessor
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.LockStatus.LockStatus
@@ -30,14 +31,15 @@ import vinyldns.core.Messages._
import vinyldns.mysql.TransactionProvider
object MembershipService {
- def apply(dataAccessor: ApiDataAccessor): MembershipService =
+ def apply(dataAccessor: ApiDataAccessor,emailConfig:ValidEmailConfig): MembershipService =
new MembershipService(
dataAccessor.groupRepository,
dataAccessor.userRepository,
dataAccessor.membershipRepository,
dataAccessor.zoneRepository,
dataAccessor.groupChangeRepository,
- dataAccessor.recordSetRepository
+ dataAccessor.recordSetRepository,
+ emailConfig
)
}
@@ -47,7 +49,8 @@ class MembershipService(
membershipRepo: MembershipRepository,
zoneRepo: ZoneRepository,
groupChangeRepo: GroupChangeRepository,
- recordSetRepo: RecordSetRepository
+ recordSetRepo: RecordSetRepository,
+ validDomains: ValidEmailConfig
) extends MembershipServiceAlgebra with TransactionProvider {
import MembershipValidations._
@@ -58,6 +61,7 @@ class MembershipService(
val nonAdminMembers = inputGroup.memberIds.diff(adminMembers)
for {
_ <- groupValidation(newGroup)
+ _ <- emailValidation(newGroup.email)
_ <- hasMembersAndAdmins(newGroup).toResult
_ <- groupWithSameNameDoesNotExist(newGroup.name)
_ <- usersExist(newGroup.memberIds)
@@ -65,6 +69,11 @@ class MembershipService(
} yield newGroup
}
+ def listEmailDomains(authPrincipal: AuthPrincipal): Result[List[String]] = {
+ val validEmailDomains = validDomains.valid_domains
+ IO(validEmailDomains).toResult
+ }
+
def updateGroup(
groupId: String,
name: String,
@@ -78,6 +87,7 @@ class MembershipService(
existingGroup <- getExistingGroup(groupId)
newGroup = existingGroup.withUpdates(name, email, description, memberIds, adminUserIds)
_ <- groupValidation(newGroup)
+ _ <- emailValidation(newGroup.email)
_ <- canEditGroup(existingGroup, authPrincipal).toResult
addedAdmins = newGroup.adminUserIds.diff(existingGroup.adminUserIds)
// new non-admin members ++ admins converted to non-admins
@@ -244,8 +254,10 @@ class MembershipService(
.getGroupChange(groupChangeId)
.toResult[Option[GroupChange]]
_ <- isGroupChangePresent(result).toResult
- _ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
- groupChangeMessage <- determineGroupDifference(Seq(result.get))
+ _ <- canSeeGroupChange(result.get.newGroup.id, authPrincipal).toResult
+ allUserIds = getGroupUserIds(Seq(result.get))
+ allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
+ groupChangeMessage <- determineGroupDifference(Seq(result.get), allUserMap)
groupChanges = (groupChangeMessage, Seq(result.get)).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
userIds = Seq(result.get).map(_.userId).toSet
users <- getUsers(userIds).map(_.users)
@@ -254,16 +266,18 @@ class MembershipService(
def getGroupActivity(
groupId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int,
authPrincipal: AuthPrincipal
): Result[ListGroupChangesResponse] =
for {
- _ <- canSeeGroup(groupId, authPrincipal).toResult
+ _ <- canSeeGroupChange(groupId, authPrincipal).toResult
result <- groupChangeRepo
.getGroupChanges(groupId, startFrom, maxItems)
.toResult[ListGroupChangesResults]
- groupChangeMessage <- determineGroupDifference(result.changes)
+ allUserIds = getGroupUserIds(result.changes)
+ allUserMap <- getUsers(allUserIds).map(_.users.map(x => x.id -> x.userName).toMap.withDefaultValue("unknown user"))
+ groupChangeMessage <- determineGroupDifference(result.changes, allUserMap)
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
userIds = result.changes.map(_.userId).toSet
users <- getUsers(userIds).map(_.users)
@@ -271,11 +285,25 @@ class MembershipService(
} yield ListGroupChangesResponse(
groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
startFrom,
- result.lastEvaluatedTimeStamp,
+ result.nextId,
maxItems
)
- def determineGroupDifference(groupChange: Seq[GroupChange]): Result[Seq[String]] = {
+ def getGroupUserIds(groupChange: Seq[GroupChange]): Set[String] = {
+ var userIds: Set[String] = Set.empty[String]
+ for (change <- groupChange) {
+ if (change.oldGroup.isDefined) {
+ val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
+ val adminRemoveDifference = change.oldGroup.get.adminUserIds.diff(change.newGroup.adminUserIds)
+ val memberAddDifference = change.newGroup.memberIds.diff(change.oldGroup.get.memberIds)
+ val memberRemoveDifference = change.oldGroup.get.memberIds.diff(change.newGroup.memberIds)
+ userIds = userIds ++ adminAddDifference ++ adminRemoveDifference ++ memberAddDifference ++ memberRemoveDifference
+ }
+ }
+ userIds
+ }
+
+ def determineGroupDifference(groupChange: Seq[GroupChange], allUserMap: Map[String, String]): Result[Seq[String]] = {
var groupChangeMessage: Seq[String] = Seq.empty[String]
for (change <- groupChange) {
@@ -288,23 +316,24 @@ class MembershipService(
sb.append(s"Group email changed to '${change.newGroup.email}'. ")
}
if (change.oldGroup.get.description != change.newGroup.description) {
- sb.append(s"Group description changed to '${change.newGroup.description.get}'. ")
+ val description = if(change.newGroup.description.isEmpty) "" else change.newGroup.description.get
+ sb.append(s"Group description changed to '$description'. ")
}
val adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
if (adminAddDifference.nonEmpty) {
- sb.append(s"Group admin/s with userId/s (${adminAddDifference.mkString(",")}) added. ")
+ sb.append(s"Group admin/s with user name/s '${adminAddDifference.map(x => allUserMap(x)).mkString("','")}' added. ")
}
val adminRemoveDifference = change.oldGroup.get.adminUserIds.diff(change.newGroup.adminUserIds)
if (adminRemoveDifference.nonEmpty) {
- sb.append(s"Group admin/s with userId/s (${adminRemoveDifference.mkString(",")}) removed. ")
+ sb.append(s"Group admin/s with user name/s '${adminRemoveDifference.map(x => allUserMap(x)).mkString("','")}' removed. ")
}
val memberAddDifference = change.newGroup.memberIds.diff(change.oldGroup.get.memberIds)
if (memberAddDifference.nonEmpty) {
- sb.append(s"Group member/s with userId/s (${memberAddDifference.mkString(",")}) added. ")
+ sb.append(s"Group member/s with user name/s '${memberAddDifference.map(x => allUserMap(x)).mkString("','")}' added. ")
}
val memberRemoveDifference = change.oldGroup.get.memberIds.diff(change.newGroup.memberIds)
if (memberRemoveDifference.nonEmpty) {
- sb.append(s"Group member/s with userId/s (${memberRemoveDifference.mkString(",")}) removed. ")
+ sb.append(s"Group member/s with user name/s '${memberRemoveDifference.map(x => allUserMap(x)).mkString("','")}' removed. ")
}
groupChangeMessage = groupChangeMessage :+ sb.toString().trim
}
@@ -333,6 +362,14 @@ class MembershipService(
.orFail(UserNotFoundError(s"User $userIdentifier was not found"))
.toResult[User]
+
+ def getUserDetails(userIdentifier: String, authPrincipal: AuthPrincipal): Result[UserResponseInfo] =
+ for{
+ user <- getUser(userIdentifier,authPrincipal)
+ group <- membershipRepo.getGroupsForUser(user.id).toResult[Set[String]]
+ } yield UserResponseInfo(user.id, Some(user.userName), group)
+
+
def getUsers(
userIds: Set[String],
startFrom: Option[String] = None,
@@ -363,6 +400,37 @@ class MembershipService(
().asRight
}
}.toResult
+ // Validate email details.Email domains details are fetched from the config file.
+ def emailValidation(email: String): Result[Unit] = {
+ val emailDomains = validDomains.valid_domains
+ val numberOfDots= validDomains.number_of_dots
+ val splitEmailDomains = emailDomains.mkString(",")
+ val emailRegex ="""^(?!\.)(?!.*\.$)(?!.*\.\.)[a-zA-Z0-9._+!&-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$""".r
+ val index = email.indexOf('@');
+ val emailSplit = if(index != -1){
+ email.substring(index+1,email.length)}
+ val wildcardEmailDomains=if(splitEmailDomains.contains("*")){
+ emailDomains.map(x=>x.replaceAllLiterally("*",""))}
+ else emailDomains
+
+ Option(email) match {
+ case Some(value) if (emailRegex.findFirstIn(value) != None && emailSplit.toString.count(_ == '.')>0)=>
+
+ if ((emailDomains.contains(emailSplit) || emailDomains.isEmpty || wildcardEmailDomains.exists(x => emailSplit.toString.endsWith(x)))&&
+ emailSplit.toString.count(_ == '.')<=numberOfDots)
+ ().asRight
+ else {
+ if(emailSplit.toString.count(_ == '.')>numberOfDots){
+ EmailValidationError(DotsValidationErrorMsg + " " + numberOfDots).asLeft
+ }
+ else {
+ EmailValidationError(EmailValidationErrorMsg + " " + wildcardEmailDomains.mkString(",")).asLeft
+ }
+ }
+ case _ =>
+ EmailValidationError(InvalidEmailValidationErrorMsg).asLeft
+ }}.toResult
+
def groupWithSameNameDoesNotExist(name: String): Result[Unit] =
groupRepo
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
index c71d62c2a..3c2971acd 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipServiceAlgebra.scala
@@ -25,6 +25,8 @@ trait MembershipServiceAlgebra {
def createGroup(inputGroup: Group, authPrincipal: AuthPrincipal): Result[Group]
+ def listEmailDomains(authPrincipal: AuthPrincipal):Result[List[String]]
+
def updateGroup(
groupId: String,
name: String,
@@ -61,7 +63,7 @@ trait MembershipServiceAlgebra {
def getGroupActivity(
groupId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int,
authPrincipal: AuthPrincipal
): Result[ListGroupChangesResponse]
@@ -76,4 +78,9 @@ trait MembershipServiceAlgebra {
userIdentifier: String,
authPrincipal: AuthPrincipal
): Result[User]
+
+ def getUserDetails(
+ userIdentifier: String,
+ authPrincipal: AuthPrincipal
+ ): Result[UserResponseInfo]
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipValidations.scala
index 898caff91..5a72d8837 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/membership/MembershipValidations.scala
@@ -45,6 +45,11 @@ object MembershipValidations {
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails
}
+ def canSeeGroupChange(groupId: String, authPrincipal: AuthPrincipal): Either[Throwable, Unit] =
+ ensuring(NotAuthorizedError("Not authorized")) {
+ authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin
+ }
+
def isGroupChangePresent(groupChange: Option[GroupChange]): Either[Throwable, Unit] =
ensuring(InvalidGroupRequestError("Invalid Group Change ID")) {
groupChange.isDefined
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/ListRecordSetChangesResponse.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/ListRecordSetChangesResponse.scala
index 0dcda1985..26136d102 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/ListRecordSetChangesResponse.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/ListRecordSetChangesResponse.scala
@@ -17,13 +17,13 @@
package vinyldns.api.domain.record
import vinyldns.api.domain.zone.RecordSetChangeInfo
-import vinyldns.core.domain.record.ListRecordSetChangesResults
+import vinyldns.core.domain.record.{ListFailedRecordSetChangesResults, ListRecordSetChangesResults, RecordSetChange}
case class ListRecordSetChangesResponse(
zoneId: String,
recordSetChanges: List[RecordSetChangeInfo] = Nil,
- nextId: Option[String],
- startFrom: Option[String],
+ nextId: Option[Int],
+ startFrom: Option[Int],
maxItems: Int
)
@@ -41,3 +41,43 @@ object ListRecordSetChangesResponse {
listResults.maxItems
)
}
+
+case class ListRecordSetHistoryResponse(
+ zoneId: Option[String],
+ recordSetChanges: List[RecordSetChangeInfo] = Nil,
+ nextId: Option[Int],
+ startFrom: Option[Int],
+ maxItems: Int
+ )
+
+object ListRecordSetHistoryResponse {
+ def apply(
+ zoneId: Option[String],
+ listResults: ListRecordSetChangesResults,
+ info: List[RecordSetChangeInfo]
+ ): ListRecordSetHistoryResponse =
+ ListRecordSetHistoryResponse(
+ zoneId,
+ info,
+ listResults.nextId,
+ listResults.startFrom,
+ listResults.maxItems
+ )
+}
+
+case class ListFailedRecordSetChangesResponse(
+ failedRecordSetChanges: List[RecordSetChange] = Nil,
+ nextId: Int,
+ startFrom: Int,
+ maxItems: Int
+ )
+
+object ListFailedRecordSetChangesResponse {
+ def apply(
+ ListFailedRecordSetChanges: ListFailedRecordSetChangesResults
+ ): ListFailedRecordSetChangesResponse =
+ ListFailedRecordSetChangesResponse(
+ ListFailedRecordSetChanges.items,
+ ListFailedRecordSetChanges.nextId,
+ ListFailedRecordSetChanges.startFrom,
+ ListFailedRecordSetChanges.maxItems)}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetCacheService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetCacheService.scala
index 6c90aaa1a..a0d6bd06f 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetCacheService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetCacheService.scala
@@ -19,7 +19,7 @@ package vinyldns.api.domain.record
import cats.effect.IO
import org.slf4j.LoggerFactory
import scalikejdbc.DB
-import vinyldns.core.domain.record.{NameSort, ListRecordSetResults, RecordSetCacheRepository, RecordSetRepository}
+import vinyldns.core.domain.record.{ListRecordSetResults, NameSort, RecordSetCacheRepository, RecordSetRepository, RecordTypeSort}
import vinyldns.mysql.TransactionProvider
@@ -30,7 +30,7 @@ class RecordSetCacheService(recordSetRepository: RecordSetRepository,
final def populateRecordSetCache(nextId: Option[String] = None): IO[ListRecordSetResults] = {
logger.info(s"Populating recordset data. Starting at $nextId")
for {
- result <- recordSetRepository.listRecordSets(None, nextId, Some(1000), None, None, None, NameSort.ASC)
+ result <- recordSetRepository.listRecordSets(None, nextId, Some(1000), None, None, None, NameSort.ASC, RecordTypeSort.ASC)
_ <- executeWithinTransaction { db: DB =>
IO {
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetChangeGenerator.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetChangeGenerator.scala
index a87bd6c1e..786103073 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetChangeGenerator.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetChangeGenerator.scala
@@ -113,6 +113,25 @@ object RecordSetChangeGenerator extends DnsConversions {
singleBatchChangeIds = singleBatchChangeIds
)
+ def forOutOfSync(
+ recordSet: RecordSet,
+ zone: Zone,
+ userId: String,
+ singleBatchChangeIds: List[String]
+ ): RecordSetChange =
+ RecordSetChange(
+ zone = zone,
+ recordSet = recordSet.copy(
+ name = relativize(recordSet.name, zone.name),
+ status = RecordSetStatus.PendingDelete,
+ updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS))
+ ),
+ userId = userId,
+ changeType = RecordSetChangeType.Sync,
+ updates = Some(recordSet),
+ singleBatchChangeIds = singleBatchChangeIds
+ )
+
def forDelete(
recordSet: RecordSet,
zone: Zone,
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala
index 67f3a0ac1..3c4b9809f 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetService.scala
@@ -27,6 +27,7 @@ import vinyldns.core.domain.zone.{Zone, ZoneCommandResult, ZoneRepository}
import vinyldns.core.queue.MessageQueue
import cats.data._
import cats.effect.IO
+import org.slf4j.{Logger, LoggerFactory}
import org.xbill.DNS.ReverseMap
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig, HighValueDomainConfig}
import vinyldns.api.domain.DomainValidations.{validateIpv4Address, validateIpv6Address}
@@ -35,6 +36,8 @@ import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
import vinyldns.core.domain.backend.{Backend, BackendResolver}
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
+import vinyldns.core.notifier.{AllNotifiers, Notification}
import scala.util.matching.Regex
@@ -48,7 +51,8 @@ object RecordSetService {
highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex],
- useRecordSetCache: Boolean
+ useRecordSetCache: Boolean,
+ notifiers: AllNotifiers
): RecordSetService =
new RecordSetService(
dataAccessor.zoneRepository,
@@ -64,7 +68,9 @@ object RecordSetService {
highValueDomainConfig,
dottedHostsConfig,
approvedNameServers,
- useRecordSetCache
+ useRecordSetCache,
+ notifiers
+
)
}
@@ -82,12 +88,18 @@ class RecordSetService(
highValueDomainConfig: HighValueDomainConfig,
dottedHostsConfig: DottedHostsConfig,
approvedNameServers: List[Regex],
- useRecordSetCache: Boolean
+ useRecordSetCache: Boolean,
+ notifiers: AllNotifiers
) extends RecordSetServiceAlgebra {
import RecordSetValidations._
import accessValidation._
+ val logger: Logger = LoggerFactory.getLogger(classOf[RecordSetService])
+
+ val approverOwnerShipTransferStatus = List(OwnerShipTransferStatus.ManuallyApproved , OwnerShipTransferStatus.AutoApproved, OwnerShipTransferStatus.ManuallyRejected)
+ val requestorOwnerShipTransferStatus = List(OwnerShipTransferStatus.Cancelled , OwnerShipTransferStatus.Requested, OwnerShipTransferStatus.PendingReview)
+
def addRecordSet(recordSet: RecordSet, auth: AuthPrincipal): Result[ZoneCommandResult] =
for {
zone <- getZone(recordSet.zoneId)
@@ -142,13 +154,31 @@ class RecordSetService(
_ <- unchangedRecordName(existing, recordSet, zone).toResult
_ <- unchangedRecordType(existing, recordSet).toResult
_ <- unchangedZoneId(existing, recordSet).toResult
+ _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))
+ && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
+ unchangedRecordSet(existing, recordSet).toResult else ().toResult
+ _ <- if(existing.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") == OwnerShipTransferStatus.Cancelled
+ && !auth.isSuper) {
+ recordSetOwnerShipApproveStatus(recordSet).toResult
+ } else ().toResult
+ _ = logger.info(s"updated recordsetgroupchange: ${recordSet.recordSetGroupChange}")
+ _ = logger.info(s"existing recordsetgroupchange: ${existing.recordSetGroupChange}")
+ recordSet <- updateRecordSetGroupChangeStatus(recordSet, existing, zone)
change <- RecordSetChangeGenerator.forUpdate(existing, recordSet, zone, Some(auth)).toResult
// because changes happen to the RS in forUpdate itself, converting 1st and validating on that
rsForValidations = change.recordSet
+ superUserCanUpdateOwnerGroup = canSuperUserUpdateOwnerGroup(existing, recordSet, zone, auth)
_ <- isNotHighValueDomain(recordSet, zone, highValueDomainConfig).toResult
- _ <- canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId).toResult
+ _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))
+ && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None"))) ().toResult
+ else canUpdateRecordSet(auth, existing.name, existing.typ, zone, existing.ownerGroupId, superUserCanUpdateOwnerGroup).toResult
ownerGroup <- getGroupIfProvided(rsForValidations.ownerGroupId)
- _ <- canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
+ _ <- if(requestorOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))
+ && !auth.isSuper && !auth.isGroupMember(existing.ownerGroupId.getOrElse("None")))
+ canUseOwnerGroup(rsForValidations.recordSetGroupChange.map(_.requestedOwnerGroupId).get, ownerGroup, auth).toResult
+ else if(approverOwnerShipTransferStatus.contains(recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse(""))
+ && !auth.isSuper) canUseOwnerGroup(existing.ownerGroupId, ownerGroup, auth).toResult
+ else canUseOwnerGroup(rsForValidations.ownerGroupId, ownerGroup, auth).toResult
_ <- notPending(existing).toResult
existingRecordsWithName <- recordSetRepository
.getRecordSetsByName(zone.id, rsForValidations.name)
@@ -183,6 +213,11 @@ class RecordSetService(
_ <- if(existing.name == rsForValidations.name) ().toResult else if(allowedZoneList.contains(zone.name)) checkAllowedDots(allowedDotsLimit, rsForValidations, zone).toResult else ().toResult
_ <- if(allowedZoneList.contains(zone.name)) isNotApexEndsWithDot(rsForValidations, zone).toResult else ().toResult
_ <- messageQueue.send(change).toResult[Unit]
+ _ <- if(recordSet.recordSetGroupChange != None &&
+ recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.None &&
+ recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.AutoApproved)
+ notifiers.notify(Notification(change)).toResult
+ else ().toResult
} yield change
def deleteRecordSet(
@@ -201,6 +236,65 @@ class RecordSetService(
_ <- messageQueue.send(change).toResult[Unit]
} yield change
+ //update ownership transfer is zone is shared
+ def updateRecordSetGroupChangeStatus(recordSet: RecordSet, existing: RecordSet, zone: Zone): Result[RecordSet] = {
+ val existingOwnerShipTransfer = existing.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
+ val ownerShipTransfer = recordSet.recordSetGroupChange.getOrElse(OwnerShipTransfer.apply(OwnerShipTransferStatus.None, Some("none")))
+ if (recordSet.recordSetGroupChange != None &&
+ ownerShipTransfer.ownerShipTransferStatus != OwnerShipTransferStatus.None)
+ if (zone.shared){
+ if (approverOwnerShipTransferStatus.contains(ownerShipTransfer.ownerShipTransferStatus)) {
+ val recordSetOwnerApproval =
+ ownerShipTransfer.ownerShipTransferStatus match {
+ case OwnerShipTransferStatus.ManuallyApproved =>
+ recordSet.copy(ownerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId,
+ recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved,
+ requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
+ case OwnerShipTransferStatus.ManuallyRejected =>
+ recordSet.copy(
+ recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected,
+ requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
+ case OwnerShipTransferStatus.AutoApproved =>
+ recordSet.copy(
+ ownerGroupId = ownerShipTransfer.requestedOwnerGroupId,
+ recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,
+ requestedOwnerGroupId = ownerShipTransfer.requestedOwnerGroupId)))
+
+ case _ => recordSet.copy(
+ recordSetGroupChange = Some(ownerShipTransfer.copy(
+ ownerShipTransferStatus = OwnerShipTransferStatus.None,
+ requestedOwnerGroupId = Some("null"))))
+ }
+ for {
+ recordSet <- recordSetOwnerApproval.toResult
+ } yield recordSet
+ }
+ else {
+ val recordSetOwnerRequest =
+ ownerShipTransfer.ownerShipTransferStatus match {
+ case OwnerShipTransferStatus.Cancelled =>
+ recordSet.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
+ requestedOwnerGroupId = existingOwnerShipTransfer.requestedOwnerGroupId)))
+ case OwnerShipTransferStatus.Requested | OwnerShipTransferStatus.PendingReview => recordSet.copy(
+ recordSetGroupChange = Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview)))
+ }
+ for {
+ recordSet <- recordSetOwnerRequest.toResult
+ } yield recordSet
+ }
+ } else for {
+ _ <- unchangedRecordSetOwnershipStatus(recordSet, existing).toResult
+ } yield recordSet.copy(
+ recordSetGroupChange = Some(ownerShipTransfer.copy(
+ ownerShipTransferStatus = OwnerShipTransferStatus.None,
+ requestedOwnerGroupId = Some("null"))))
+ else recordSet.copy(
+ recordSetGroupChange = Some(ownerShipTransfer.copy(
+ ownerShipTransferStatus = OwnerShipTransferStatus.None,
+ requestedOwnerGroupId = Some("null")))).toResult
+ }
+
// For dotted hosts. Check if a record that may conflict with dotted host exist or not
def recordFQDNDoesNotExist(newRecordSet: RecordSet, zone: Zone): IO[Boolean] = {
// Use fqdn for searching through `recordset` mysql table to see if it already exist
@@ -382,6 +476,14 @@ class RecordSetService(
groupName <- getGroupName(recordSet.ownerGroupId)
} yield RecordSetInfo(recordSet, groupName)
+ def getRecordSetCount(zoneId: String, authPrincipal: AuthPrincipal): Result[RecordSetCount] = {
+ for {
+ zone <- getZone(zoneId)
+ _ <- canSeeZone(authPrincipal, zone).toResult
+ count <- recordSetRepository.getRecordSetCount(zoneId).toResult
+ } yield RecordSetCount(count)
+ }
+
def getRecordSetByZone(
recordSetId: String,
zoneId: String,
@@ -407,7 +509,8 @@ class RecordSetService(
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse] =
for {
_ <- validRecordNameFilterLength(recordNameFilter).toResult
@@ -420,7 +523,8 @@ class RecordSetService(
Some(formattedRecordNameFilter),
recordTypeFilter,
recordOwnerGroupFilter,
- nameSort
+ nameSort,
+ recordTypeSort
)
.toResult[ListRecordSetResults]
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
@@ -458,7 +562,8 @@ class RecordSetService(
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse] = {
for {
_ <- validRecordNameFilterLength(recordNameFilter).toResult
@@ -483,7 +588,8 @@ class RecordSetService(
Some(formattedRecordNameFilter),
recordTypeFilter,
recordOwnerGroupFilter,
- nameSort
+ nameSort,
+ recordTypeSort
).toResult[ListRecordSetResults]
}
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
@@ -511,7 +617,8 @@ class RecordSetService(
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListRecordSetsByZoneResponse] =
for {
zone <- getZone(zoneId)
@@ -524,7 +631,8 @@ class RecordSetService(
recordNameFilter,
recordTypeFilter,
recordOwnerGroupFilter,
- nameSort
+ nameSort,
+ recordTypeSort
)
.toResult[ListRecordSetResults]
rsOwnerGroupIds = recordSetResults.recordSets.flatMap(_.ownerGroupId).toSet
@@ -539,7 +647,8 @@ class RecordSetService(
recordSetResults.recordNameFilter,
recordSetResults.recordTypeFilter,
recordSetResults.recordOwnerGroupFilter,
- recordSetResults.nameSort
+ recordSetResults.nameSort,
+ recordSetResults.recordTypeSort
)
def getRecordSetChange(
@@ -568,18 +677,61 @@ class RecordSetService(
def listRecordSetChanges(
zoneId: String,
- startFrom: Option[String] = None,
+ startFrom: Option[Int] = None,
maxItems: Int = 100,
authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse] =
+ for {
+ zone <- getZone(zoneId)
+ _ <- canSeeZone(authPrincipal, zone).toResult
+ recordSetChangesResults <- recordChangeRepository
+ .listRecordSetChanges(Some(zone.id), startFrom, maxItems, None, None)
+ .toResult[ListRecordSetChangesResults]
+ recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
+ } yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
+
+ def listRecordSetChangeHistory(
+ zoneId: Option[String] = None,
+ startFrom: Option[Int] = None,
+ maxItems: Int = 100,
+ fqdn: Option[String] = None,
+ recordType: Option[RecordType] = None,
+ authPrincipal: AuthPrincipal
+ ): Result[ListRecordSetHistoryResponse] =
for {
- zone <- getZone(zoneId)
+ zone <- getZone(zoneId.get)
_ <- canSeeZone(authPrincipal, zone).toResult
recordSetChangesResults <- recordChangeRepository
- .listRecordSetChanges(zone.id, startFrom, maxItems)
+ .listRecordSetChanges(zoneId, startFrom, maxItems, fqdn, recordType)
.toResult[ListRecordSetChangesResults]
recordSetChangesInfo <- buildRecordSetChangeInfo(recordSetChangesResults.items)
- } yield ListRecordSetChangesResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
+ } yield ListRecordSetHistoryResponse(zoneId, recordSetChangesResults, recordSetChangesInfo)
+
+ def listFailedRecordSetChanges(
+ authPrincipal: AuthPrincipal,
+ zoneId: Option[String] = None,
+ startFrom: Int= 0,
+ maxItems: Int = 100
+ ): Result[ListFailedRecordSetChangesResponse] =
+ for {
+ recordSetChangesFailedResults <- recordChangeRepository
+ .listFailedRecordSetChanges(zoneId, maxItems, startFrom)
+ .toResult[ListFailedRecordSetChangesResults]
+ _ <- zoneAccess(recordSetChangesFailedResults.items, authPrincipal).toResult
+ } yield
+ ListFailedRecordSetChangesResponse(
+ recordSetChangesFailedResults.items,
+ recordSetChangesFailedResults.nextId,
+ startFrom,
+ maxItems)
+
+ def zoneAccess(
+ RecordSetCh: List[RecordSetChange],
+ auth: AuthPrincipal
+ ): List[Result[Unit]] =
+ RecordSetCh.map { zn =>
+ canSeeZone(auth, zn.zone).toResult
+ }
def getZone(zoneId: String): Result[Zone] =
zoneRepository
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetServiceAlgebra.scala
index 5d7089196..d21374b93 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetServiceAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetServiceAlgebra.scala
@@ -17,12 +17,13 @@
package vinyldns.api.domain.record
import vinyldns.api.Interfaces.Result
-import vinyldns.api.domain.zone.RecordSetInfo
+import vinyldns.api.domain.zone.{RecordSetCount, RecordSetInfo}
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.zone.ZoneCommandResult
import vinyldns.api.route.{ListGlobalRecordSetsResponse, ListRecordSetsByZoneResponse}
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.domain.record.{RecordSet, RecordSetChange}
trait RecordSetServiceAlgebra {
@@ -54,7 +55,8 @@ trait RecordSetServiceAlgebra {
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupId: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse]
/**
@@ -76,7 +78,8 @@ trait RecordSetServiceAlgebra {
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupId: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse]
def listRecordSetsByZone(
@@ -87,7 +90,8 @@ trait RecordSetServiceAlgebra {
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupId: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListRecordSetsByZoneResponse]
def getRecordSetChange(
@@ -98,9 +102,27 @@ trait RecordSetServiceAlgebra {
def listRecordSetChanges(
zoneId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int,
authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse]
+ def listRecordSetChangeHistory(
+ zoneId: Option[String],
+ startFrom: Option[Int],
+ maxItems: Int,
+ fqdn: Option[String],
+ recordType: Option[RecordType],
+ authPrincipal: AuthPrincipal
+ ): Result[ListRecordSetHistoryResponse]
+
+ def listFailedRecordSetChanges(
+ authPrincipal: AuthPrincipal,
+ zoneId: Option[String],
+ startFrom: Int,
+ maxItems: Int
+ ): Result[ListFailedRecordSetChangesResponse]
+
+ def getRecordSetCount(zoneId: String, authPrincipal: AuthPrincipal): Result[RecordSetCount]
+
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala
index cbbd35b60..e3b80a4d3 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/record/RecordSetValidations.scala
@@ -20,13 +20,14 @@ import cats.syntax.either._
import vinyldns.api.Interfaces._
import vinyldns.api.backend.dns.DnsConversions
import vinyldns.api.config.HighValueDomainConfig
+import vinyldns.api.domain.DomainValidations.validateIpv4Address
import vinyldns.api.domain._
import vinyldns.core.domain.DomainHelpers._
import vinyldns.core.domain.record.RecordType._
import vinyldns.api.domain.zone._
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.Group
-import vinyldns.core.domain.record.{RecordSet, RecordType}
+import vinyldns.core.domain.record.{OwnerShipTransferStatus, RecordSet, RecordType}
import vinyldns.core.domain.zone.Zone
import vinyldns.core.Messages._
@@ -236,6 +237,16 @@ object RecordSetValidations {
)
}
+ val isNotIPv4inCname = {
+ ensuring(
+ RecordSetValidation(
+ s"""Invalid CNAME: ${newRecordSet.records.head.toString.dropRight(1)}, valid CNAME record data cannot be an IP address."""
+ )
+ )(
+ validateIpv4Address(newRecordSet.records.head.toString.dropRight(1)).isInvalid
+ )
+ }
+
for {
_ <- isNotOrigin(
newRecordSet,
@@ -243,6 +254,7 @@ object RecordSetValidations {
"CNAME RecordSet cannot have name '@' because it points to zone origin"
)
_ <- noRecordWithName
+ _ <- isNotIPv4inCname
_ <- RDataWithConsecutiveDots
_ <- checkForDot(newRecordSet, zone, existingRecordSet, recordFqdnDoesNotExist, dottedHostZoneConfig, isRecordTypeAndUserAllowed, allowedDotsLimit)
} yield ()
@@ -425,10 +437,66 @@ object RecordSetValidations {
InvalidRequest("Cannot update RecordSet's zone ID.")
)
+ /**
+ * Checks of the user is a superuser, the zone is shared, and the only record attribute being changed
+ * is the record owner group.
+ */
+ def canSuperUserUpdateOwnerGroup(
+ existing: RecordSet,
+ updates: RecordSet,
+ zone: Zone,
+ auth: AuthPrincipal
+ ): Boolean =
+ (updates.ownerGroupId != existing.ownerGroupId
+ && updates.zoneId == existing.zoneId
+ && updates.name == existing.name
+ && updates.typ == existing.typ
+ && updates.ttl == existing.ttl
+ && updates.records == existing.records
+ && zone.shared
+ && auth.isSuper)
+
def validRecordNameFilterLength(recordNameFilter: String): Either[Throwable, Unit] =
ensuring(onError = InvalidRequest(RecordNameFilterError)) {
val searchRegex = "[a-zA-Z0-9].*[a-zA-Z0-9]+".r
val wildcardRegex = raw"^\s*[*%].*[*%]\s*$$".r
searchRegex.findFirstIn(recordNameFilter).isDefined && wildcardRegex.findFirstIn(recordNameFilter).isEmpty
}
+
+ def unchangedRecordSet(
+ existing: RecordSet,
+ updates: RecordSet
+ ): Either[Throwable, Unit] =
+ Either.cond(
+ updates.typ == existing.typ &&
+ updates.records == existing.records &&
+ updates.id == existing.id &&
+ updates.zoneId == existing.zoneId &&
+ updates.name == existing.name &&
+ updates.ownerGroupId == existing.ownerGroupId &&
+ updates.ttl == existing.ttl,
+ (),
+ InvalidRequest("Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer")
+ )
+
+ def recordSetOwnerShipApproveStatus(
+ updates: RecordSet,
+ ): Either[Throwable, Unit] =
+ Either.cond(
+ updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.ManuallyApproved &&
+ updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.AutoApproved &&
+ updates.recordSetGroupChange.map(_.ownerShipTransferStatus).getOrElse("") != OwnerShipTransferStatus.ManuallyRejected,
+ (),
+ InvalidRequest("Cannot update RecordSet OwnerShip Status when request is cancelled.")
+ )
+
+ def unchangedRecordSetOwnershipStatus(
+ updates: RecordSet,
+ existing: RecordSet
+ ): Either[Throwable, Unit] =
+ Either.cond(
+ updates.recordSetGroupChange == existing.recordSetGroupChange || existing.recordSetGroupChange.isEmpty,
+ (),
+ InvalidRequest("Cannot update RecordSet OwnerShip Status when zone is not shared.")
+ )
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/AclRuleOrdering.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/AclRuleOrdering.scala
index 972cede06..1ebad43f4 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/AclRuleOrdering.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/AclRuleOrdering.scala
@@ -16,7 +16,7 @@
package vinyldns.api.domain.zone
-import com.aaronbedra.orchard.CIDR
+import com.comcast.ip4s.Cidr
import vinyldns.core.domain.record.RecordType
import vinyldns.core.domain.zone.ACLRule
@@ -61,7 +61,7 @@ object ACLRuleOrdering extends ACLRuleOrdering {
object PTRACLRuleOrdering extends ACLRuleOrdering {
def sortableRecordMaskValue(rule: ACLRule): Int = {
val slash = rule.recordMask match {
- case Some(cidrRule) => CIDR.valueOf(cidrRule).getMask
+ case Some(cidrRule) => Cidr.fromString(cidrRule).get.prefixBits
case None => 0
}
128 - slash
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala
index 90997b927..7c84acb0c 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ListZoneChangesResponse.scala
@@ -17,7 +17,7 @@
package vinyldns.api.domain.zone
import vinyldns.api.domain.zone
-import vinyldns.core.domain.zone.{ListZoneChangesResults, ZoneChange}
+import vinyldns.core.domain.zone.{ListFailedZoneChangesResults, ListZoneChangesResults, ZoneChange}
case class ListZoneChangesResponse(
zoneId: String,
@@ -37,3 +37,29 @@ object ListZoneChangesResponse {
listResults.maxItems
)
}
+
+case class ListDeletedZoneChangesResponse(
+ zonesDeletedInfo: List[ZoneChangeDeletedInfo],
+ zoneChangeFilter: Option[String] = None,
+ nextId: Option[String] = None,
+ startFrom: Option[String] = None,
+ maxItems: Int = 100,
+ ignoreAccess: Boolean = false
+)
+
+case class ListFailedZoneChangesResponse(
+ failedZoneChanges: List[ZoneChange] = Nil,
+ nextId: Int,
+ startFrom: Int,
+ maxItems: Int
+ )
+
+object ListFailedZoneChangesResponse {
+ def apply(listResults: ListFailedZoneChangesResults): ListFailedZoneChangesResponse =
+ zone.ListFailedZoneChangesResponse(
+ listResults.items,
+ listResults.nextId,
+ listResults.startFrom,
+ listResults.maxItems
+ )
+}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneChangeGenerator.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneChangeGenerator.scala
index 4de2d2bdb..5da5b5381 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneChangeGenerator.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneChangeGenerator.scala
@@ -61,6 +61,14 @@ object ZoneChangeGenerator {
ZoneChangeStatus.Pending
)
+ def forSyncs(zone: Zone): ZoneChange =
+ ZoneChange(
+ zone.copy(updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), status = ZoneStatus.Syncing),
+ zone.scheduleRequestor.get,
+ ZoneChangeType.AutomatedSync,
+ ZoneChangeStatus.Pending
+ )
+
def forDelete(zone: Zone, authPrincipal: AuthPrincipal): ZoneChange =
ZoneChange(
zone.copy(updated = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS)), status = ZoneStatus.Deleted),
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala
index 7ebdd77ea..94b663cf2 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneProtocol.scala
@@ -21,8 +21,8 @@ import vinyldns.core.domain.record.RecordSetChangeStatus.RecordSetChangeStatus
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
import vinyldns.core.domain.record.RecordSetStatus.RecordSetStatus
import vinyldns.core.domain.record.RecordType.RecordType
-import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange}
-import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneConnection}
+import vinyldns.core.domain.record.{RecordData, RecordSet, RecordSetChange, OwnerShipTransfer}
+import vinyldns.core.domain.zone.{ACLRuleInfo, AccessLevel, Zone, ZoneACL, ZoneChange, ZoneConnection}
import vinyldns.core.domain.zone.AccessLevel.AccessLevel
import vinyldns.core.domain.zone.ZoneStatus.ZoneStatus
@@ -44,6 +44,8 @@ case class ZoneInfo(
adminGroupName: String,
latestSync: Option[Instant],
backendId: Option[String],
+ recurrenceSchedule: Option[String],
+ scheduleRequestor: Option[String],
accessLevel: AccessLevel
)
@@ -70,10 +72,34 @@ object ZoneInfo {
adminGroupName = groupName,
latestSync = zone.latestSync,
backendId = zone.backendId,
+ recurrenceSchedule = zone.recurrenceSchedule,
+ scheduleRequestor = zone.scheduleRequestor,
accessLevel = accessLevel
)
}
+case class ZoneDetails(
+ name: String,
+ email: String,
+ status: ZoneStatus,
+ adminGroupId: String,
+ adminGroupName: String,
+ )
+
+object ZoneDetails {
+ def apply(
+ zone: Zone,
+ groupName: String,
+ ): ZoneDetails =
+ ZoneDetails(
+ name = zone.name,
+ email = zone.email,
+ status = zone.status,
+ adminGroupId = zone.adminGroupId,
+ adminGroupName = groupName,
+ )
+}
+
case class ZoneSummaryInfo(
name: String,
email: String,
@@ -90,6 +116,8 @@ case class ZoneSummaryInfo(
adminGroupName: String,
latestSync: Option[Instant],
backendId: Option[String],
+ recurrenceSchedule: Option[String],
+ scheduleRequestor: Option[String],
accessLevel: AccessLevel
)
@@ -111,6 +139,29 @@ object ZoneSummaryInfo {
adminGroupName = groupName,
latestSync = zone.latestSync,
zone.backendId,
+ recurrenceSchedule = zone.recurrenceSchedule,
+ scheduleRequestor = zone.scheduleRequestor,
+ accessLevel = accessLevel
+ )
+}
+
+case class ZoneChangeDeletedInfo(
+ zoneChange: ZoneChange,
+ adminGroupName: String,
+ userName: String,
+ accessLevel: AccessLevel
+ )
+
+object ZoneChangeDeletedInfo {
+ def apply(zoneChange: List[ZoneChange],
+ groupName: String,
+ userName: String,
+ accessLevel: AccessLevel)
+ : ZoneChangeDeletedInfo =
+ ZoneChangeDeletedInfo(
+ zoneChange= zoneChange,
+ groupName = groupName,
+ userName = userName,
accessLevel = accessLevel
)
}
@@ -129,6 +180,7 @@ case class RecordSetListInfo(
accessLevel: AccessLevel,
ownerGroupId: Option[String],
ownerGroupName: Option[String],
+ recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String]
)
@@ -148,6 +200,7 @@ object RecordSetListInfo {
accessLevel = accessLevel,
ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = recordSet.ownerGroupName,
+ recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn
)
}
@@ -165,6 +218,7 @@ case class RecordSetInfo(
account: String,
ownerGroupId: Option[String],
ownerGroupName: Option[String],
+ recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String]
)
@@ -183,6 +237,7 @@ object RecordSetInfo {
account = recordSet.account,
ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = groupName,
+ recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn
)
}
@@ -200,6 +255,7 @@ case class RecordSetGlobalInfo(
account: String,
ownerGroupId: Option[String],
ownerGroupName: Option[String],
+ recordSetGroupChange: Option[OwnerShipTransfer],
fqdn: Option[String],
zoneName: String,
zoneShared: Boolean
@@ -225,6 +281,7 @@ object RecordSetGlobalInfo {
account = recordSet.account,
ownerGroupId = recordSet.ownerGroupId,
ownerGroupName = groupName,
+ recordSetGroupChange = recordSet.recordSetGroupChange,
fqdn = recordSet.fqdn,
zoneName = zoneName,
zoneShared = zoneShared
@@ -266,9 +323,12 @@ case class ListZonesResponse(
startFrom: Option[String] = None,
nextId: Option[String] = None,
maxItems: Int = 100,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
)
+case class RecordSetCount( count: Int = 0 )
+
// Errors
case class InvalidRequest(msg: String) extends Throwable(msg)
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneRecordValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneRecordValidations.scala
index 11f19f496..dfd499ff8 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneRecordValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneRecordValidations.scala
@@ -19,13 +19,7 @@ package vinyldns.api.domain.zone
import cats.implicits._
import cats.data._
import com.comcast.ip4s.IpAddress
-import com.comcast.ip4s.interop.cats.implicits._
-import vinyldns.core.domain.{
- DomainHelpers,
- DomainValidationError,
- HighValueDomainError,
- RecordRequiresManualReview
-}
+import vinyldns.core.domain.{DomainHelpers, DomainValidationError, HighValueDomainError, RecordRequiresManualReview}
import vinyldns.core.domain.record.{NSData, RecordSet}
import scala.util.matching.Regex
@@ -41,7 +35,7 @@ object ZoneRecordValidations {
/* Checks to see if an ip address is part of the ip address list */
def isIpInIpList(ipList: List[IpAddress], ipToTest: String): Boolean =
- IpAddress(ipToTest).exists(ip => ipList.exists(_ === ip))
+ IpAddress.fromString(ipToTest).exists(ip => ipList.exists(_ === ip))
/* Checks to see if an individual ns data is part of the approved server list */
def isApprovedNameServer(
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala
index 3b8d66a36..6f9ef6829 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneService.scala
@@ -23,11 +23,16 @@ import vinyldns.api.Interfaces
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.api.repository.ApiDataAccessor
import vinyldns.core.crypto.CryptoAlgebra
-import vinyldns.core.domain.membership.{Group, GroupRepository, User, UserRepository}
+import vinyldns.core.domain.membership.{Group, GroupRepository, ListUsersResults, User, UserRepository}
import vinyldns.core.domain.zone._
import vinyldns.core.queue.MessageQueue
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
import vinyldns.core.domain.backend.BackendResolver
+import com.cronutils.model.definition.CronDefinition
+import com.cronutils.model.definition.CronDefinitionBuilder
+import com.cronutils.parser.CronParser
+import com.cronutils.model.CronType
+import vinyldns.api.domain.membership.MembershipService
object ZoneService {
def apply(
@@ -37,7 +42,8 @@ object ZoneService {
zoneValidations: ZoneValidations,
accessValidation: AccessValidationsAlgebra,
backendResolver: BackendResolver,
- crypto: CryptoAlgebra
+ crypto: CryptoAlgebra,
+ membershipService:MembershipService
): ZoneService =
new ZoneService(
dataAccessor.zoneRepository,
@@ -49,7 +55,8 @@ object ZoneService {
zoneValidations,
accessValidation,
backendResolver,
- crypto
+ crypto,
+ membershipService
)
}
@@ -63,7 +70,8 @@ class ZoneService(
zoneValidations: ZoneValidations,
accessValidation: AccessValidationsAlgebra,
backendResolver: BackendResolver,
- crypto: CryptoAlgebra
+ crypto: CryptoAlgebra,
+ membershipService:MembershipService
) extends ZoneServiceAlgebra {
import accessValidation._
@@ -76,12 +84,17 @@ class ZoneService(
): Result[ZoneCommandResult] =
for {
_ <- isValidZoneAcl(createZoneInput.acl).toResult
+ _ <- membershipService.emailValidation(createZoneInput.email)
_ <- connectionValidator.isValidBackendId(createZoneInput.backendId).toResult
_ <- validateSharedZoneAuthorized(createZoneInput.shared, auth.signedInUser).toResult
_ <- zoneDoesNotExist(createZoneInput.name)
_ <- adminGroupExists(createZoneInput.adminGroupId)
+ _ <- if(createZoneInput.recurrenceSchedule.isDefined) canScheduleZoneSync(auth).toResult else IO.unit.toResult
+ isCronStringValid = if(createZoneInput.recurrenceSchedule.isDefined) isValidCronString(createZoneInput.recurrenceSchedule.get) else true
+ _ <- validateCronString(isCronStringValid).toResult
_ <- canChangeZone(auth, createZoneInput.name, createZoneInput.adminGroupId).toResult
- zoneToCreate = Zone(createZoneInput, auth.isTestUser)
+ createdZoneInput = if(createZoneInput.recurrenceSchedule.isDefined) createZoneInput.copy(scheduleRequestor = Some(auth.signedInUser.userName)) else createZoneInput
+ zoneToCreate = Zone(createdZoneInput, auth.isTestUser)
_ <- connectionValidator.validateZoneConnections(zoneToCreate)
createZoneChange <- ZoneChangeGenerator.forAdd(zoneToCreate, auth).toResult
_ <- messageQueue.send(createZoneChange).toResult[Unit]
@@ -90,6 +103,7 @@ class ZoneService(
def updateZone(updateZoneInput: UpdateZoneInput, auth: AuthPrincipal): Result[ZoneCommandResult] =
for {
_ <- isValidZoneAcl(updateZoneInput.acl).toResult
+ _ <- membershipService.emailValidation(updateZoneInput.email)
_ <- connectionValidator.isValidBackendId(updateZoneInput.backendId).toResult
existingZone <- getZoneOrFail(updateZoneInput.id)
_ <- validateSharedZoneAuthorized(
@@ -98,10 +112,14 @@ class ZoneService(
auth.signedInUser
).toResult
_ <- canChangeZone(auth, existingZone.name, existingZone.adminGroupId).toResult
+ _ <- if(updateZoneInput.recurrenceSchedule.isDefined) canScheduleZoneSync(auth).toResult else IO.unit.toResult
+ isCronStringValid = if(updateZoneInput.recurrenceSchedule.isDefined) isValidCronString(updateZoneInput.recurrenceSchedule.get) else true
+ _ <- validateCronString(isCronStringValid).toResult
_ <- adminGroupExists(updateZoneInput.adminGroupId)
// if admin group changes, this confirms user has access to new group
_ <- canChangeZone(auth, updateZoneInput.name, updateZoneInput.adminGroupId).toResult
- zoneWithUpdates = Zone(updateZoneInput, existingZone)
+ updatedZoneInput = if(updateZoneInput.recurrenceSchedule.isDefined) updateZoneInput.copy(scheduleRequestor = Some(auth.signedInUser.userName)) else updateZoneInput
+ zoneWithUpdates = Zone(updatedZoneInput, existingZone)
_ <- validateZoneConnectionIfChanged(zoneWithUpdates, existingZone)
updateZoneChange <- ZoneChangeGenerator
.forUpdate(zoneWithUpdates, existingZone, auth, crypto)
@@ -135,6 +153,12 @@ class ZoneService(
accessLevel = getZoneAccess(auth, zone)
} yield ZoneInfo(zone, aclInfo, groupName, accessLevel)
+ def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails] =
+ for {
+ zone <- getZoneOrFail(zoneId)
+ groupName <- getGroupName(zone.adminGroupId)
+ } yield ZoneDetails(zone, groupName)
+
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] =
for {
zone <- getZoneByNameOrFail(ensureTrailingDot(zoneName))
@@ -150,7 +174,8 @@ class ZoneService(
startFrom: Option[String] = None,
maxItems: Int = 100,
searchByAdminGroup: Boolean = false,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): Result[ListZonesResponse] = {
if(!searchByAdminGroup || nameFilter.isEmpty){
for {
@@ -159,21 +184,22 @@ class ZoneService(
nameFilter,
startFrom,
maxItems,
- ignoreAccess
- )
- zones = listZonesResult.zones
- groupIds = zones.map(_.adminGroupId).toSet
- groups <- groupRepository.getGroups(groupIds)
- zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
- } yield ListZonesResponse(
- zoneSummaryInfos,
- listZonesResult.zonesFilter,
- listZonesResult.startFrom,
- listZonesResult.nextId,
- listZonesResult.maxItems,
- listZonesResult.ignoreAccess
+ ignoreAccess,
+ includeReverse
)
- }
+ zones = listZonesResult.zones
+ groupIds = zones.map(_.adminGroupId).toSet
+ groups <- groupRepository.getGroups(groupIds)
+ zoneSummaryInfos = zoneSummaryInfoMapping(zones, authPrincipal, groups)
+ } yield ListZonesResponse(
+ zoneSummaryInfos,
+ listZonesResult.zonesFilter,
+ listZonesResult.startFrom,
+ listZonesResult.nextId,
+ listZonesResult.maxItems,
+ listZonesResult.ignoreAccess,
+ listZonesResult.includeReverse
+ )}
else {
for {
groupIds <- getGroupsIdsByName(nameFilter.get)
@@ -182,7 +208,8 @@ class ZoneService(
startFrom,
maxItems,
groupIds,
- ignoreAccess
+ ignoreAccess,
+ includeReverse
)
zones = listZonesResult.zones
groups <- groupRepository.getGroups(groupIds)
@@ -193,11 +220,64 @@ class ZoneService(
listZonesResult.startFrom,
listZonesResult.nextId,
listZonesResult.maxItems,
- listZonesResult.ignoreAccess
+ listZonesResult.ignoreAccess,
+ listZonesResult.includeReverse
)
}
}.toResult
+ def listDeletedZones(
+ authPrincipal: AuthPrincipal,
+ nameFilter: Option[String] = None,
+ startFrom: Option[String] = None,
+ maxItems: Int = 100,
+ ignoreAccess: Boolean = false
+ ): Result[ListDeletedZoneChangesResponse] = {
+ for {
+ listZonesChangeResult <- zoneChangeRepository.listDeletedZones(
+ authPrincipal,
+ nameFilter,
+ startFrom,
+ maxItems,
+ ignoreAccess
+ )
+ zoneChanges = listZonesChangeResult.zoneDeleted
+ groupIds = zoneChanges.map(_.zone.adminGroupId).toSet
+ groups <- groupRepository.getGroups(groupIds)
+ userId = zoneChanges.map(_.userId).toSet
+ users <- userRepository.getUsers(userId,None,None)
+ zoneDeleteSummaryInfos = ZoneChangeDeletedInfoMapping(zoneChanges, authPrincipal, groups, users)
+ } yield {
+ ListDeletedZoneChangesResponse(
+ zoneDeleteSummaryInfos,
+ listZonesChangeResult.zoneChangeFilter,
+ listZonesChangeResult.nextId,
+ listZonesChangeResult.startFrom,
+ listZonesChangeResult.maxItems,
+ listZonesChangeResult.ignoreAccess
+ )
+ }
+ }.toResult
+
+ private def ZoneChangeDeletedInfoMapping(
+ zoneChange: List[ZoneChange],
+ auth: AuthPrincipal,
+ groups: Set[Group],
+ users: ListUsersResults
+ ): List[ZoneChangeDeletedInfo] =
+ zoneChange.map { zc =>
+ val groupName = groups.find(_.id == zc.zone.adminGroupId) match {
+ case Some(group) => group.name
+ case None => "Unknown group name"
+ }
+ val userName = users.users.find(_.id == zc.userId) match {
+ case Some(user) => user.userName
+ case None => "Unknown user name"
+ }
+ val zoneAccess = getZoneAccess(auth, zc.zone)
+ ZoneChangeDeletedInfo(zc, groupName,userName, zoneAccess)
+ }
+
def zoneSummaryInfoMapping(
zones: List[Zone],
auth: AuthPrincipal,
@@ -220,12 +300,38 @@ class ZoneService(
): Result[ListZoneChangesResponse] =
for {
zone <- getZoneOrFail(zoneId)
- _ <- canSeeZone(authPrincipal, zone).toResult
+ _ <- canSeeZoneChange(authPrincipal, zone).toResult
zoneChangesResults <- zoneChangeRepository
.listZoneChanges(zone.id, startFrom, maxItems)
.toResult[ListZoneChangesResults]
} yield ListZoneChangesResponse(zone.id, zoneChangesResults)
+ def listFailedZoneChanges(
+ authPrincipal: AuthPrincipal,
+ startFrom: Int= 0,
+ maxItems: Int = 100
+ ): Result[ListFailedZoneChangesResponse] =
+ for {
+ zoneChangesFailedResults <- zoneChangeRepository
+ .listFailedZoneChanges(maxItems, startFrom)
+ .toResult[ListFailedZoneChangesResults]
+ _ <- zoneAccess(zoneChangesFailedResults.items, authPrincipal).toResult
+ } yield
+ ListFailedZoneChangesResponse(
+ zoneChangesFailedResults.items,
+ zoneChangesFailedResults.nextId,
+ startFrom,
+ maxItems
+ )
+
+ def zoneAccess(
+ zoneCh: List[ZoneChange],
+ auth: AuthPrincipal
+ ): List[Result[Unit]] =
+ zoneCh.map { zn =>
+ canSeeZone(auth, zn.zone).toResult
+ }
+
def addACLRule(
zoneId: String,
aclRuleInfo: ACLRuleInfo,
@@ -276,6 +382,28 @@ class ZoneService(
def getBackendIds(): Result[List[String]] =
backendResolver.ids.toList.toResult
+ def isValidCronString(maybeString: String): Boolean = {
+ val isValid = try {
+ val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
+ val parser: CronParser = new CronParser(cronDefinition)
+ val quartzCron = parser.parse(maybeString)
+ quartzCron.validate
+ true
+ }
+ catch {
+ case _: Exception =>
+ false
+ }
+ isValid
+ }
+
+ def validateCronString(isValid: Boolean): Either[Throwable, Unit] =
+ ensuring(
+ InvalidRequest("Invalid cron expression. Please enter a valid cron expression in 'recurrenceSchedule'.")
+ )(
+ isValid
+ )
+
def zoneDoesNotExist(zoneName: String): Result[Unit] =
zoneRepository
.getZoneByName(zoneName)
@@ -289,6 +417,13 @@ class ZoneService(
}
.toResult
+ def canScheduleZoneSync(auth: AuthPrincipal): Either[Throwable, Unit] =
+ ensuring(
+ NotAuthorizedError(s"User '${auth.signedInUser.userName}' is not authorized to schedule zone sync in this zone.")
+ )(
+ auth.isSystemAdmin
+ )
+
def adminGroupExists(groupId: String): Result[Unit] =
groupRepository
.getGroup(groupId)
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala
index cbcec8f92..daede2241 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneServiceAlgebra.scala
@@ -35,6 +35,8 @@ trait ZoneServiceAlgebra {
def getZone(zoneId: String, auth: AuthPrincipal): Result[ZoneInfo]
+ def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails]
+
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo]
def listZones(
@@ -43,9 +45,18 @@ trait ZoneServiceAlgebra {
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean,
- ignoreAccess: Boolean
+ ignoreAccess: Boolean,
+ includeReverse: Boolean
): Result[ListZonesResponse]
+ def listDeletedZones(
+ authPrincipal: AuthPrincipal,
+ nameFilter: Option[String],
+ startFrom: Option[String],
+ maxItems: Int,
+ ignoreAccess: Boolean
+ ): Result[ListDeletedZoneChangesResponse]
+
def listZoneChanges(
zoneId: String,
authPrincipal: AuthPrincipal,
@@ -67,4 +78,9 @@ trait ZoneServiceAlgebra {
def getBackendIds(): Result[List[String]]
+ def listFailedZoneChanges(
+ authPrincipal: AuthPrincipal,
+ startFrom: Int,
+ maxItems: Int
+ ): Result[ListFailedZoneChangesResponse]
}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandler.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandler.scala
new file mode 100644
index 000000000..f8629caf7
--- /dev/null
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandler.scala
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018 Comcast Cable Communications Management, LLC
+ *
+ * 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 vinyldns.api.domain.zone
+
+import cats.effect.IO
+import com.cronutils.model.CronType
+import com.cronutils.model.definition.{CronDefinition, CronDefinitionBuilder}
+import com.cronutils.model.time.ExecutionTime
+import com.cronutils.parser.CronParser
+import org.slf4j.LoggerFactory
+import vinyldns.core.domain.zone.{Zone, ZoneChange, ZoneRepository}
+import java.time.{Instant, ZoneId}
+import java.time.temporal.ChronoUnit
+
+object ZoneSyncScheduleHandler {
+ private val logger = LoggerFactory.getLogger("ZoneSyncScheduleHandler")
+
+ def zoneSyncScheduler(zoneRepository: ZoneRepository): IO[Set[ZoneChange]] = {
+ for {
+ zones <- zoneRepository.getAllZonesWithSyncSchedule
+ zoneScheduleIds = getZonesWithSchedule(zones.toList)
+ zoneChanges <- getZoneChanges(zoneRepository, zoneScheduleIds)
+ } yield zoneChanges
+ }
+
+ def getZoneChanges(zoneRepository: ZoneRepository, zoneScheduleIds: List[String]): IO[Set[ZoneChange]] = {
+ if(zoneScheduleIds.nonEmpty) {
+ for{
+ getZones <- zoneRepository.getZones(zoneScheduleIds.toSet)
+ syncZoneChange = getZones.map(zone => ZoneChangeGenerator.forSyncs(zone))
+ } yield syncZoneChange
+ } else {
+ IO(Set.empty)
+ }
+ }
+
+ def getZonesWithSchedule(zone: List[Zone]): List[String] = {
+ var zonesWithSchedule: List[String] = List.empty
+ for(z <- zone) {
+ if (z.recurrenceSchedule.isDefined) {
+ val now = Instant.now().atZone(ZoneId.of("UTC"))
+ val cronDefinition: CronDefinition = CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)
+ val parser: CronParser = new CronParser(cronDefinition)
+ val executionTime: ExecutionTime = ExecutionTime.forCron(parser.parse(z.recurrenceSchedule.get))
+ val nextExecution = executionTime.nextExecution(now).get()
+ val diff = ChronoUnit.SECONDS.between(now, nextExecution)
+ if (diff == 1) {
+ zonesWithSchedule = zonesWithSchedule :+ z.id
+ logger.info("Zones with sync schedule: " + zonesWithSchedule)
+ } else {
+ List.empty
+ }
+ } else {
+ List.empty
+ }
+ }
+ zonesWithSchedule
+ }
+
+}
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneValidations.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneValidations.scala
index 7c6283b9e..8d34af5e5 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneValidations.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneValidations.scala
@@ -17,7 +17,7 @@
package vinyldns.api.domain.zone
import cats.syntax.either._
-import com.aaronbedra.orchard.CIDR
+import com.comcast.ip4s.Cidr
import java.time.Instant
import java.time.temporal.ChronoUnit
import vinyldns.api.Interfaces.ensuring
@@ -57,10 +57,10 @@ class ZoneValidations(syncDelayMillis: Int) {
def aclRuleMaskIsValid(rule: ACLRule): Either[Throwable, Unit] =
rule.recordMask match {
case Some(mask) if rule.recordTypes == Set(RecordType.PTR) =>
- Try(CIDR.valueOf(mask)) match {
+ Try(Cidr.fromString(mask).get) match {
case Success(_) => Right(())
- case Failure(e) =>
- InvalidRequest(s"PTR types must have no mask or a valid CIDR mask: ${e.getMessage}").asLeft
+ case Failure(_) =>
+ InvalidRequest(s"PTR types must have no mask or a valid CIDR mask: Invalid CIDR block").asLeft
}
case Some(_) if rule.recordTypes.contains(RecordType.PTR) =>
InvalidRequest("Multiple record types including PTR must have no mask").asLeft
diff --git a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneViewLoader.scala b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneViewLoader.scala
index f841d48fc..e1cf02d0d 100644
--- a/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneViewLoader.scala
+++ b/modules/api/src/main/scala/vinyldns/api/domain/zone/ZoneViewLoader.scala
@@ -20,7 +20,7 @@ import cats.effect._
import org.slf4j.LoggerFactory
import vinyldns.api.backend.dns.DnsConversions
import vinyldns.core.domain.backend.Backend
-import vinyldns.core.domain.record.{NameSort, RecordSetCacheRepository, RecordSetRepository}
+import vinyldns.core.domain.record.{NameSort, RecordSetCacheRepository, RecordSetRepository, RecordTypeSort}
import vinyldns.core.domain.zone.Zone
import vinyldns.core.route.Monitored
@@ -69,7 +69,8 @@ case class VinylDNSZoneViewLoader(
recordNameFilter = None,
recordTypeFilter = None,
recordOwnerGroupFilter = None,
- nameSort = NameSort.ASC
+ nameSort = NameSort.ASC,
+ recordTypeSort = RecordTypeSort.ASC
)
.map { result =>
VinylDNSZoneViewLoader.logger.info(
diff --git a/modules/api/src/main/scala/vinyldns/api/engine/RecordSetChangeHandler.scala b/modules/api/src/main/scala/vinyldns/api/engine/RecordSetChangeHandler.scala
index 553e7a06d..31c57febd 100644
--- a/modules/api/src/main/scala/vinyldns/api/engine/RecordSetChangeHandler.scala
+++ b/modules/api/src/main/scala/vinyldns/api/engine/RecordSetChangeHandler.scala
@@ -23,6 +23,7 @@ import scalikejdbc.DB
import vinyldns.api.backend.dns.DnsProtocol.TryAgain
import vinyldns.api.domain.record.RecordSetChangeGenerator
import vinyldns.api.domain.record.RecordSetHelpers._
+import vinyldns.core.Messages.{nonExistentRecordDataDeleteMessage, nonExistentRecordDeleteMessage}
import vinyldns.core.domain.backend.{Backend, BackendResponse}
import vinyldns.core.domain.batch.{BatchChangeRepository, SingleChange}
import vinyldns.core.domain.record._
@@ -35,6 +36,11 @@ object RecordSetChangeHandler extends TransactionProvider {
private implicit val cs: ContextShift[IO] =
IO.contextShift(scala.concurrent.ExecutionContext.global)
+ private val outOfSyncFailureMessage: String = "This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."
+ private val incompatibleRecordFailureMessage: String = "Incompatible record in DNS."
+ private val syncZoneMessage: String = "This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."
+ private val recordConflictMessage: String = "Conflict due to the record having the same name as an NS record in the same zone. Please create the record using the DNS service the NS record has been delegated to (ex. AWS r53), or use a different record name."
+
final case class Requeue(change: RecordSetChange) extends Throwable
def apply(
@@ -87,15 +93,31 @@ object RecordSetChangeHandler extends TransactionProvider {
): IO[Unit] =
executeWithinTransaction { db: DB =>
for {
- _ <- recordSetRepository.apply(db, changeSet)
- _ <- recordChangeRepository.save(db, changeSet)
- _ <- recordSetCacheRepository.save(db, changeSet)
// Update single changes within this transaction to rollback the changes made to recordset and record change repo
// when exception occurs while updating single changes
singleBatchChanges <- batchChangeRepository.getSingleChanges(
recordSetChange.singleBatchChangeIds
)
singleChangeStatusUpdates = updateBatchStatuses(singleBatchChanges, completedState.change)
+ updatedChangeSet = if (singleChangeStatusUpdates.size == 1) {
+ // Filter out RecordSetChange from changeSet if systemMessage matches
+ val filteredChangeSetChanges = changeSet.changes.filterNot { recordSetChange =>
+ // Find the corresponding singleChangeStatusUpdate by recordChangeId
+ singleChangeStatusUpdates.exists { singleChange =>
+ singleChange.recordChangeId.contains(recordSetChange.id) &&
+ singleChange.systemMessage.exists(msg =>
+ msg == nonExistentRecordDeleteMessage || msg == nonExistentRecordDataDeleteMessage
+ )
+ }
+ }
+ // Create a new ChangeSet with filtered changes
+ changeSet.copy(changes = filteredChangeSetChanges)
+ } else {
+ changeSet
+ }
+ _ <- recordSetRepository.apply(db, updatedChangeSet)
+ _ <- recordChangeRepository.save(db, updatedChangeSet)
+ _ <- recordSetCacheRepository.save(db, updatedChangeSet)
_ <- batchChangeRepository.updateSingleChanges(singleChangeStatusUpdates)
} yield ()
}
@@ -106,7 +128,7 @@ object RecordSetChangeHandler extends TransactionProvider {
): List[SingleChange] =
recordSetChange.status match {
case RecordSetChangeStatus.Complete =>
- singleChanges.map(_.complete(recordSetChange.systemMessage, recordSetChange.id, recordSetChange.recordSet.id))
+ singleChanges.map(_.complete(recordSetChange.id, recordSetChange.recordSet.id))
case RecordSetChangeStatus.Failed =>
singleChanges.map(_.withProcessingError(recordSetChange.systemMessage, recordSetChange.id))
case _ => singleChanges
@@ -157,15 +179,6 @@ object RecordSetChangeHandler extends TransactionProvider {
def isDnsMatch(dnsResult: List[RecordSet], recordSet: RecordSet, zoneName: String): Boolean =
dnsResult.exists(matches(_, recordSet, zoneName))
- def isRecordExist(existingRecords: List[RecordSet], change: RecordSetChange): Boolean = {
- var isExists : Boolean = false
- existingRecords.foreach(recordData=>
- for (record<-change.recordSet.records)
- if (recordData.records.contains(record)) isExists= true
- else isExists= false )
- isExists
- }
-
// Determine processing status by comparing request against disposition of DNS backend
def getProcessingStatus(
change: RecordSetChange,
@@ -174,9 +187,9 @@ object RecordSetChangeHandler extends TransactionProvider {
change.changeType match {
case RecordSetChangeType.Create =>
if (existingRecords.isEmpty) ReadyToApply(change)
- else if (isDnsMatch(existingRecords, change.recordSet, change.zone.name) || isRecordExist(existingRecords,change))
- AlreadyApplied(change) //Record exists in DNS
- else Failure(change, "Incompatible record in DNS.")
+ else if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
+ AlreadyApplied(change)
+ else Failure(change, incompatibleRecordFailureMessage)
case RecordSetChangeType.Update =>
if (isDnsMatch(existingRecords, change.recordSet, change.zone.name))
@@ -190,14 +203,23 @@ object RecordSetChangeHandler extends TransactionProvider {
else
Failure(
change,
- "This record set is out of sync with the DNS backend; " +
- "sync this zone before attempting to update this record set."
+ outOfSyncFailureMessage
)
}
case RecordSetChangeType.Delete =>
if (existingRecords.nonEmpty) ReadyToApply(change) // we have a record set, move forward
else AlreadyApplied(change) // we did not find the record set, so already applied
+
+ case RecordSetChangeType.Sync =>
+ if (existingRecords.nonEmpty) {
+ Failure(
+ change,
+ outOfSyncFailureMessage
+ )
+ } else {
+ AlreadyApplied(change)
+ }
}
}
@@ -397,11 +419,25 @@ object RecordSetChangeHandler extends TransactionProvider {
case AlreadyApplied(_) => Completed(change.successful)
case ReadyToApply(_) => Validated(change)
case Failure(_, message) =>
- Completed(
- change.failed(
- s"""Failed validating update to DNS for change "${change.id}": "${change.recordSet.name}": """ + message
+ if(message == outOfSyncFailureMessage || message == incompatibleRecordFailureMessage){
+ Completed(
+ change.failed(
+ syncZoneMessage
+ )
)
- )
+ } else if (message == "referral") {
+ Completed(
+ change.failed(
+ recordConflictMessage
+ )
+ )
+ } else {
+ Completed(
+ change.failed(
+ s"""Failed validating update to DNS for change "${change.id}": "${change.recordSet.name}": """ + message
+ )
+ )
+ }
case Retry(_) => Retrying(change)
}
diff --git a/modules/api/src/main/scala/vinyldns/api/engine/ZoneChangeHandler.scala b/modules/api/src/main/scala/vinyldns/api/engine/ZoneChangeHandler.scala
index 988ffff71..326a428d9 100644
--- a/modules/api/src/main/scala/vinyldns/api/engine/ZoneChangeHandler.scala
+++ b/modules/api/src/main/scala/vinyldns/api/engine/ZoneChangeHandler.scala
@@ -17,12 +17,16 @@
package vinyldns.api.engine
import cats.effect.IO
+import org.slf4j.{Logger, LoggerFactory}
import scalikejdbc.DB
import vinyldns.api.engine.ZoneSyncHandler.executeWithinTransaction
import vinyldns.core.domain.record.{RecordSetCacheRepository, RecordSetRepository}
import vinyldns.core.domain.zone._
object ZoneChangeHandler {
+
+ private implicit val logger: Logger = LoggerFactory.getLogger("vinyldns.engine.ZoneChangeHandler")
+
def apply(
zoneRepository: ZoneRepository,
zoneChangeRepository: ZoneChangeRepository,
@@ -54,6 +58,7 @@ object ZoneChangeHandler {
zoneChangeRepository.save(zoneChange.copy(status = ZoneChangeStatus.Synced))
}
case Right(_) =>
+ logger.info(s"Saving zone change with id: '${zoneChange.id}', zone name: '${zoneChange.zone.name}'")
zoneChangeRepository.save(zoneChange.copy(status = ZoneChangeStatus.Synced))
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/engine/ZoneSyncHandler.scala b/modules/api/src/main/scala/vinyldns/api/engine/ZoneSyncHandler.scala
index 3db1dff7f..b4d2a924c 100644
--- a/modules/api/src/main/scala/vinyldns/api/engine/ZoneSyncHandler.scala
+++ b/modules/api/src/main/scala/vinyldns/api/engine/ZoneSyncHandler.scala
@@ -29,6 +29,7 @@ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
import vinyldns.core.route.Monitored
import vinyldns.mysql.TransactionProvider
+import java.io.{PrintWriter, StringWriter}
object ZoneSyncHandler extends DnsConversions with Monitored with TransactionProvider {
@@ -78,6 +79,7 @@ object ZoneSyncHandler extends DnsConversions with Monitored with TransactionPro
)
)
case Right(_) =>
+ logger.info(s"Saving zone sync details for zone change with id: '${zoneChange.id}', zone name: '${zoneChange.zone.name}'")
zoneChangeRepository.save(zoneChange)
}
@@ -169,9 +171,10 @@ object ZoneSyncHandler extends DnsConversions with Monitored with TransactionPro
}
}.attempt.map {
case Left(e: Throwable) =>
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
logger.error(
- s"Encountered error syncing ; zoneName='${zoneChange.zone.name}'; zoneChange='${zoneChange.id}'",
- e
+ s"Encountered error syncing ; zoneName='${zoneChange.zone.name}'; zoneChange='${zoneChange.id}'. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}"
)
// We want to just move back to an active status, do not update latest sync
zoneChange.copy(
diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala
index dd0318a75..3abcd5ca8 100644
--- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala
+++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifier.scala
@@ -18,33 +18,25 @@ package vinyldns.api.notifier.email
import vinyldns.core.notifier.{Notification, Notifier}
import cats.effect.IO
-import vinyldns.core.domain.batch.{
- BatchChange,
- BatchChangeApprovalStatus,
- SingleAddChange,
- SingleChange,
- SingleDeleteRRSetChange
-}
-import vinyldns.core.domain.membership.UserRepository
-import vinyldns.core.domain.membership.User
+import cats.implicits._
+import cats.effect.IO
+import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleDeleteRRSetChange}
+import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository}
import org.slf4j.LoggerFactory
+
import javax.mail.internet.{InternetAddress, MimeMessage}
import javax.mail.{Address, Message, Session}
-
import scala.util.Try
-import vinyldns.core.domain.record.AData
-import vinyldns.core.domain.record.AAAAData
-import vinyldns.core.domain.record.CNAMEData
-import vinyldns.core.domain.record.MXData
-import vinyldns.core.domain.record.TXTData
-import vinyldns.core.domain.record.PTRData
-import vinyldns.core.domain.record.RecordData
+import vinyldns.core.domain.record.{AAAAData, AData, CNAMEData, MXData, OwnerShipTransferStatus, PTRData, RecordData, RecordSetChange, TXTData}
+import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
+
import java.time.format.{DateTimeFormatter, FormatStyle}
import vinyldns.core.domain.batch.BatchChangeStatus._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus._
+
import java.time.ZoneId
-class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository)
+class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepository: UserRepository, groupRepository: GroupRepository)
extends Notifier {
private val logger = LoggerFactory.getLogger(classOf[EmailNotifier])
@@ -52,12 +44,15 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
def notify(notification: Notification[_]): IO[Unit] =
notification.change match {
case bc: BatchChange => sendBatchChangeNotification(bc)
+ case rsc: RecordSetChange => sendRecordSetOwnerTransferNotification(rsc)
case _ => IO.unit
}
- def send(addresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
+
+ def send(toAddresses: Address*)(ccAddresses: Address*)(buildMessage: Message => Message): IO[Unit] = IO {
val message = new MimeMessage(session)
- message.setRecipients(Message.RecipientType.TO, addresses.toArray)
+ message.setRecipients(Message.RecipientType.TO, toAddresses.toArray)
+ message.setRecipients(Message.RecipientType.CC, ccAddresses.toArray)
message.setFrom(config.from)
buildMessage(message)
message.saveChanges()
@@ -67,10 +62,10 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
transport.close()
}
- def sendBatchChangeNotification(bc: BatchChange): IO[Unit] =
+ def sendBatchChangeNotification(bc: BatchChange): IO[Unit] = {
userRepository.getUser(bc.userId).flatMap {
- case Some(UserWithEmail(email)) =>
- send(email) { message =>
+ case Some(UserWithEmail(email)) =>
+ send(email)() { message =>
message.setSubject(s"VinylDNS Batch change ${bc.id} results")
message.setContent(formatBatchChange(bc), "text/html")
message
@@ -81,9 +76,58 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("")}"
)
}
- case None => IO { logger.warn(s"Unable to find user: ${bc.userId}") }
+ case None => IO {
+ logger.warn(s"Unable to find user: ${bc.userId}")
+ }
case _ => IO.unit
}
+ }
+
+ def sendRecordSetOwnerTransferNotification(rsc: RecordSetChange): IO[Unit] = {
+ for {
+ toGroup <- groupRepository.getGroup(rsc.recordSet.ownerGroupId.getOrElse(""))
+ ccGroup <- groupRepository.getGroup(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("")).getOrElse(""))
+ _ <- toGroup match {
+ case Some(group) =>
+ group.memberIds.toList.traverse { id =>
+ userRepository.getUser(id).flatMap {
+ case Some(UserWithEmail(toEmail)) =>
+ ccGroup match {
+ case Some(ccg) =>
+ ccg.memberIds.toList.traverse { id =>
+ userRepository.getUser(id).flatMap {
+ case Some(ccUser) =>
+ val ccEmail = ccUser.email.getOrElse("")
+ send(toEmail)(new InternetAddress(ccEmail)) { message =>
+ message.setSubject(s"VinylDNS RecordSet change ${rsc.id} results")
+ message.setContent(formatRecordSetChange(rsc), "text/html")
+ message
+ }
+ case None =>
+ IO.unit
+ }
+ }
+ case None => IO.unit
+ }
+ case Some(user: User) if user.email.isDefined =>
+ IO {
+ logger.warn(
+ s"Unable to properly parse email for ${user.id}: ${user.email.getOrElse("")}"
+ )
+ }
+ case None =>
+ IO {
+ logger.warn(s"Unable to find user: ${rsc.userId}")
+ }
+ case _ =>
+ IO.unit
+ }
+ }
+ case None => IO.unit // Handle case where toGroup is None
+ }
+ } yield ()
+ }
+
def formatBatchChange(bc: BatchChange): String = {
val sb = new StringBuilder
@@ -93,7 +137,7 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
| ${bc.comments.map(comments => s"Description: $comments").getOrElse("")}
| Created: ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(bc.createdTimestamp)}
| Id: ${bc.id}
- | Status: ${formatStatus(bc.approvalStatus, bc.status)} """.stripMargin)
+ | Status: ${formatBatchStatus(bc.approvalStatus, bc.status)} """.stripMargin)
// For manually reviewed e-mails, add additional info; e-mails are not sent for pending batch changes
if (bc.approvalStatus != AutoApproved) {
@@ -125,7 +169,8 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
sb.toString
}
- def formatStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
+
+ def formatBatchStatus(approval: BatchChangeApprovalStatus, status: BatchChangeStatus): String =
(approval, status) match {
case (ManuallyRejected, _) => "Rejected"
case (BatchChangeApprovalStatus.PendingReview, _) => "Pending Review"
@@ -133,6 +178,28 @@ class EmailNotifier(config: EmailNotifierConfig, session: Session, userRepositor
case (_, status) => status.toString
}
+ def formatRecordSetChange(rsc: RecordSetChange): String = {
+
+ val sb = new StringBuilder
+ sb.append(s"""
RecordSet Ownership Transfer
+ | Submitter: ${ userRepository.getUser(rsc.userId).map(_.get.userName)}
+ | Id: ${rsc.id}
+ | Submitted time: ${DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL).withZone(ZoneId.systemDefault()).format(rsc.created)}
+ | OwnerShip Current Group: ${rsc.recordSet.ownerGroupId.getOrElse("none")}
+ | OwnerShip Transfer Group: ${rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.getOrElse("none")).getOrElse("none")}
+ | OwnerShip Transfer Status: ${formatOwnerShipStatus(rsc.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus).get)}
+ """.stripMargin)
+ sb.toString
+ }
+
+ def formatOwnerShipStatus(status: OwnerShipTransferStatus): String =
+ status match {
+ case OwnerShipTransferStatus.ManuallyRejected => "Rejected"
+ case OwnerShipTransferStatus.PendingReview => "Pending Review"
+ case OwnerShipTransferStatus.ManuallyApproved => "Approved"
+ case OwnerShipTransferStatus.Cancelled => "Cancelled"
+ }
+
def formatSingleChange(sc: SingleChange, index: Int): String = sc match {
case SingleAddChange(
_,
diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala
index e10768e17..bf82bce92 100644
--- a/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala
+++ b/modules/api/src/main/scala/vinyldns/api/notifier/email/EmailNotifierProvider.scala
@@ -17,7 +17,7 @@
package vinyldns.api.notifier.email
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import pureconfig._
import pureconfig.generic.auto._
import pureconfig.module.catseffect.syntax._
@@ -30,13 +30,13 @@ class EmailNotifierProvider extends NotifierProvider {
private implicit val cs: ContextShift[IO] =
IO.contextShift(scala.concurrent.ExecutionContext.global)
- def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] =
+ def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for {
emailConfig <- Blocker[IO].use(
ConfigSource.fromConfig(config.settings).loadF[IO, EmailNotifierConfig](_)
)
session <- createSession(emailConfig)
- } yield new EmailNotifier(emailConfig, session, userRepository)
+ } yield new EmailNotifier(emailConfig, session, userRepository, groupRepository)
def createSession(config: EmailNotifierConfig): IO[Session] = IO {
Session.getInstance(config.smtp)
diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifier.scala b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifier.scala
index 74c34b536..b2eb3f29b 100644
--- a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifier.scala
+++ b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifier.scala
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory
import vinyldns.api.route.VinylDNSJsonProtocol
import vinyldns.core.domain.batch.BatchChange
import vinyldns.core.notifier.{Notification, Notifier}
+import java.io.{PrintWriter, StringWriter}
class SnsNotifier(config: SnsNotifierConfig, sns: AmazonSNS)
extends Notifier
@@ -52,6 +53,8 @@ class SnsNotifier(config: SnsNotifierConfig, sns: AmazonSNS)
sns.publish(request)
logger.info(s"Sending batch change success; batchChange='${bc.id}'")
}.handleErrorWith { e =>
- IO(logger.error(s"Failed sending batch change; batchChange='${bc.id}'", e))
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ IO(logger.error(s"Failed sending batch change; batchChange='${bc.id}'. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}"))
}.void
}
diff --git a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala
index dccd3c56d..bd72c5650 100644
--- a/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala
+++ b/modules/api/src/main/scala/vinyldns/api/notifier/sns/SnsNotifierProvider.scala
@@ -17,7 +17,7 @@
package vinyldns.api.notifier.sns
import vinyldns.core.notifier.{Notifier, NotifierConfig, NotifierProvider}
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import pureconfig._
import pureconfig.generic.auto._
import pureconfig.module.catseffect.syntax._
@@ -35,7 +35,7 @@ class SnsNotifierProvider extends NotifierProvider {
IO.contextShift(scala.concurrent.ExecutionContext.global)
private val logger = LoggerFactory.getLogger(classOf[SnsNotifierProvider])
- def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] =
+ def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for {
snsConfig <- Blocker[IO].use(
ConfigSource.fromConfig(config.settings).loadF[IO, SnsNotifierConfig](_)
diff --git a/modules/api/src/main/scala/vinyldns/api/repository/TestDataLoader.scala b/modules/api/src/main/scala/vinyldns/api/repository/TestDataLoader.scala
index 18b2e2617..cea672b93 100644
--- a/modules/api/src/main/scala/vinyldns/api/repository/TestDataLoader.scala
+++ b/modules/api/src/main/scala/vinyldns/api/repository/TestDataLoader.scala
@@ -22,6 +22,7 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import org.slf4j.{Logger, LoggerFactory}
import scalikejdbc.DB
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.membership._
import vinyldns.core.domain.zone._
import vinyldns.mysql.TransactionProvider
@@ -39,7 +40,7 @@ object TestDataLoader extends TransactionProvider {
id = "testuser",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "testUserAccessKey",
- secretKey = "testUserSecretKey",
+ secretKey = Encrypted("testUserSecretKey"),
firstName = Some("Test"),
lastName = Some("User"),
email = Some("test@test.com"),
@@ -50,7 +51,7 @@ object TestDataLoader extends TransactionProvider {
id = "ok",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "okAccessKey",
- secretKey = "okSecretKey",
+ secretKey = Encrypted("okSecretKey"),
firstName = Some("ok"),
lastName = Some("ok"),
email = Some("test@test.com"),
@@ -61,7 +62,7 @@ object TestDataLoader extends TransactionProvider {
id = "dummy",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "dummyAccessKey",
- secretKey = "dummySecretKey",
+ secretKey = Encrypted("dummySecretKey"),
isTest = true
)
final val sharedZoneUser = User(
@@ -69,7 +70,7 @@ object TestDataLoader extends TransactionProvider {
id = "sharedZoneUser",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "sharedZoneUserAccessKey",
- secretKey = "sharedZoneUserSecretKey",
+ secretKey = Encrypted("sharedZoneUserSecretKey"),
firstName = Some("sharedZoneUser"),
lastName = Some("sharedZoneUser"),
email = Some("test@test.com"),
@@ -80,7 +81,7 @@ object TestDataLoader extends TransactionProvider {
id = "locked",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "lockedAccessKey",
- secretKey = "lockedSecretKey",
+ secretKey = Encrypted("lockedSecretKey"),
firstName = Some("Locked"),
lastName = Some("User"),
email = Some("testlocked@test.com"),
@@ -93,7 +94,7 @@ object TestDataLoader extends TransactionProvider {
id = "dummy%03d".format(runner),
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "dummy",
- secretKey = "dummy",
+ secretKey = Encrypted("dummy"),
isTest = true
)
}
@@ -102,7 +103,7 @@ object TestDataLoader extends TransactionProvider {
id = "list-group-user",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "listGroupAccessKey",
- secretKey = "listGroupSecretKey",
+ secretKey = Encrypted("listGroupSecretKey"),
firstName = Some("list-group"),
lastName = Some("list-group"),
email = Some("test@test.com"),
@@ -114,7 +115,7 @@ object TestDataLoader extends TransactionProvider {
id = "list-zones-user",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "listZonesAccessKey",
- secretKey = "listZonesSecretKey",
+ secretKey = Encrypted("listZonesSecretKey"),
firstName = Some("list-zones"),
lastName = Some("list-zones"),
email = Some("test@test.com"),
@@ -126,7 +127,7 @@ object TestDataLoader extends TransactionProvider {
id = "history-id",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "history-key",
- secretKey = "history-secret",
+ secretKey = Encrypted("history-secret"),
firstName = Some("history-first"),
lastName = Some("history-last"),
email = Some("history@history.com"),
@@ -138,7 +139,7 @@ object TestDataLoader extends TransactionProvider {
id = "list-records-user",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "listRecordsAccessKey",
- secretKey = "listRecordsSecretKey",
+ secretKey = Encrypted("listRecordsSecretKey"),
firstName = Some("list-records"),
lastName = Some("list-records"),
email = Some("test@test.com"),
@@ -150,7 +151,7 @@ object TestDataLoader extends TransactionProvider {
id = "list-batch-summaries-id",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "listBatchSummariesAccessKey",
- secretKey = "listBatchSummariesSecretKey",
+ secretKey = Encrypted("listBatchSummariesSecretKey"),
firstName = Some("list-batch-summaries"),
lastName = Some("list-batch-summaries"),
email = Some("test@test.com"),
@@ -170,7 +171,7 @@ object TestDataLoader extends TransactionProvider {
id = "list-zero-summaries-id",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "listZeroSummariesAccessKey",
- secretKey = "listZeroSummariesSecretKey",
+ secretKey = Encrypted("listZeroSummariesSecretKey"),
firstName = Some("list-zero-summaries"),
lastName = Some("list-zero-summaries"),
email = Some("test@test.com"),
@@ -182,7 +183,7 @@ object TestDataLoader extends TransactionProvider {
id = "support-user-id",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "supportUserAccessKey",
- secretKey = "supportUserSecretKey",
+ secretKey = Encrypted("supportUserSecretKey"),
firstName = Some("support-user"),
lastName = Some("support-user"),
email = Some("test@test.com"),
@@ -190,6 +191,19 @@ object TestDataLoader extends TransactionProvider {
isTest = true
)
+ final val superUser = User(
+ userName = "super-user",
+ id = "super-user-id",
+ created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
+ accessKey = "superUserAccessKey",
+ secretKey = Encrypted("superUserSecretKey"),
+ firstName = Some("super-user"),
+ lastName = Some("super-user"),
+ email = Some("test@test.com"),
+ isSuper = true,
+ isTest = true
+ )
+
final val sharedZoneGroup = Group(
name = "testSharedZoneGroup",
id = "shared-zone-group",
@@ -276,7 +290,7 @@ object TestDataLoader extends TransactionProvider {
for {
_ <- (testUser :: okUser :: dummyUser :: sharedZoneUser :: lockedUser :: listGroupUser :: listZonesUser ::
listBatchChangeSummariesUser :: listZeroBatchChangeSummariesUser :: zoneHistoryUser :: supportUser ::
- listRecordsUser :: listOfDummyUsers).map { user =>
+ superUser :: listRecordsUser :: listOfDummyUsers).map { user =>
userRepo.save(user)
}.parSequence
// if the test shared zones exist already, clean them out
diff --git a/modules/api/src/main/scala/vinyldns/api/route/BatchChangeJsonProtocol.scala b/modules/api/src/main/scala/vinyldns/api/route/BatchChangeJsonProtocol.scala
index bc7962810..d10885c13 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/BatchChangeJsonProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/BatchChangeJsonProtocol.scala
@@ -88,6 +88,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
(
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
recordType,
+ (js \ "systemMessage").optional[String],
(js \ "ttl").optional[Long],
recordType.andThen(extractRecord(_, js \ "record"))
).mapN(AddChangeInput.apply)
@@ -114,6 +115,7 @@ trait BatchChangeJsonProtocol extends JsonValidation {
(
(js \ "inputName").required[String]("Missing BatchChangeInput.changes.inputName"),
recordType,
+ (js \ "systemMessage").optional[String],
recordData
).mapN(DeleteRRSetChangeInput.apply)
}
@@ -249,8 +251,23 @@ trait BatchChangeJsonProtocol extends JsonValidation {
js.required[MXData](
"Missing BatchChangeInput.changes.record.preference and BatchChangeInput.changes.record.exchange"
)
+ case NS => js.required[NSData]("Missing BatchChangeInput.changes.record.nsdname")
+ case SRV => js.required[SRVData](
+ "Missing BatchChangeInput.changes.record.priority and " +
+ "Missing BatchChangeInput.changes.record.weight and " +
+ "Missing BatchChangeInput.changes.record.port and " +
+ "Missing BatchChangeInput.changes.record.target"
+ )
+ case NAPTR => js.required[NAPTRData](
+ "Missing BatchChangeInput.changes.record.order and " +
+ "Missing BatchChangeInput.changes.record.preference and " +
+ "Missing BatchChangeInput.changes.record.flags and " +
+ "Missing BatchChangeInput.changes.record.service and " +
+ "Missing BatchChangeInput.changes.record.regexp and " +
+ "Missing BatchChangeInput.changes.record.replacement"
+ )
case _ =>
- s"Unsupported type $typ, valid types include: A, AAAA, CNAME, PTR, TXT, and MX".invalidNel
+ s"Unsupported type $typ, valid types include: A, AAAA, CNAME, PTR, TXT, MX, NS, SRV and NAPTR".invalidNel
}
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/route/BatchChangeRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/BatchChangeRouting.scala
index f4c021198..e9208bcbe 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/BatchChangeRouting.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/BatchChangeRouting.scala
@@ -16,13 +16,12 @@
package vinyldns.api.route
-import akka.http.scaladsl.model.StatusCodes
+import akka.http.scaladsl.model._
import akka.http.scaladsl.server.{RejectionHandler, Route, ValidationRejection}
-import vinyldns.api.config.LimitsConfig
import org.slf4j.{Logger, LoggerFactory}
-import vinyldns.api.config.ManualReviewConfig
-import vinyldns.core.domain.batch._
+import vinyldns.api.config.{LimitsConfig, ManualReviewConfig}
import vinyldns.api.domain.batch._
+import vinyldns.core.domain.batch._
class BatchChangeRoute(
batchChangeService: BatchChangeServiceAlgebra,
@@ -71,22 +70,28 @@ class BatchChangeRoute(
}
}
} ~
- (get & monitor("Endpoint.listBatchChangeSummaries")) {
- parameters(
- "startFrom".as[Int].?,
- "maxItems".as[Int].?(MAX_ITEMS_LIMIT),
- "ignoreAccess".as[Boolean].?(false),
- "approvalStatus".as[String].?
- ) {
- (
- startFrom: Option[Int],
- maxItems: Int,
- ignoreAccess: Boolean,
- approvalStatus: Option[String]
- ) =>
- {
- val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
-
+ (get & monitor("Endpoint.listBatchChangeSummaries")) {
+ parameters(
+ "userName".as[String].?,
+ "dateTimeRangeStart".as[String].?,
+ "dateTimeRangeEnd".as[String].?,
+ "startFrom".as[Int].?,
+ "maxItems".as[Int].?(MAX_ITEMS_LIMIT),
+ "ignoreAccess".as[Boolean].?(false),
+ "approvalStatus".as[String].?
+ ) {
+ (
+ userName: Option[String],
+ dateTimeRangeStart: Option[String],
+ dateTimeRangeEnd: Option[String],
+ startFrom: Option[Int],
+ maxItems: Int,
+ ignoreAccess: Boolean,
+ approvalStatus: Option[String]
+ ) =>
+ {
+ val convertApprovalStatus = approvalStatus.flatMap(BatchChangeApprovalStatus.find)
+
handleRejections(invalidQueryHandler) {
validate(
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
@@ -95,9 +100,14 @@ class BatchChangeRoute(
authenticateAndExecute(
batchChangeService.listBatchChangeSummaries(
_,
+ userName,
+ dateTimeRangeStart,
+ dateTimeRangeEnd,
startFrom,
maxItems,
ignoreAccess,
+ // TODO: Update batch status from None to its actual value when the feature is ready for release
+ None,
convertApprovalStatus
)
) { summaries =>
@@ -106,9 +116,9 @@ class BatchChangeRoute(
}
}
}
- }
+ }
}
- } ~
+ } ~
path("zones" / "batchrecordchanges" / Segment) { id =>
(get & monitor("Endpoint.getBatchChange")) {
authenticateAndExecute(batchChangeService.getBatchChange(id, _)) { chg =>
diff --git a/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala b/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala
index 1858d5685..98669c4c2 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/DnsJsonProtocol.scala
@@ -27,10 +27,12 @@ import scodec.bits.{Bases, ByteVector}
import vinyldns.api.domain.zone.{RecordSetGlobalInfo, RecordSetInfo, RecordSetListInfo}
import vinyldns.core.domain.DomainHelpers.ensureTrailingDot
import vinyldns.core.domain.DomainHelpers.removeWhitespace
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{EncryptFromJson, Encrypted, Fqdn}
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
import vinyldns.core.Messages._
+import vinyldns.core.domain.record.OwnerShipTransferStatus
+import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
trait DnsJsonProtocol extends JsonValidation {
import vinyldns.core.domain.record.RecordType._
@@ -40,12 +42,15 @@ trait DnsJsonProtocol extends JsonValidation {
UpdateZoneInputSerializer,
ZoneConnectionSerializer,
AlgorithmSerializer,
+ EncryptedSerializer,
RecordSetSerializer,
+ ownerShipTransferSerializer,
RecordSetListInfoSerializer,
RecordSetGlobalInfoSerializer,
RecordSetInfoSerializer,
RecordSetChangeSerializer,
JsonEnumV(ZoneStatus),
+ JsonEnumV(OwnerShipTransferStatus),
JsonEnumV(ZoneChangeStatus),
JsonEnumV(RecordSetStatus),
JsonEnumV(RecordSetChangeStatus),
@@ -53,6 +58,7 @@ trait DnsJsonProtocol extends JsonValidation {
JsonEnumV(ZoneChangeType),
JsonEnumV(RecordSetChangeType),
JsonEnumV(NameSort),
+ JsonEnumV(RecordTypeSort),
ASerializer,
AAAASerializer,
CNAMESerializer,
@@ -84,6 +90,18 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "id").default[String](UUID.randomUUID.toString),
(js \ "singleBatchChangeIds").default[List[String]](List())
).mapN(RecordSetChange.apply)
+
+ override def toJson(rs: RecordSetChange): JValue =
+ ("zone" -> Extraction.decompose(rs.zone)) ~
+ ("recordSet" -> Extraction.decompose(rs.recordSet)) ~
+ ("userId" -> rs.userId) ~
+ ("changeType" -> Extraction.decompose(rs.changeType)) ~
+ ("status" -> Extraction.decompose(rs.status)) ~
+ ("created" -> Extraction.decompose(rs.created)) ~
+ ("systemMessage" -> rs.systemMessage) ~
+ ("updates" -> Extraction.decompose(rs.updates)) ~
+ ("id" -> rs.id) ~
+ ("singleBatchChangeIds" -> Extraction.decompose(rs.singleBatchChangeIds))
}
case object CreateZoneInputSerializer extends ValidationSerializer[CreateZoneInput] {
@@ -99,7 +117,9 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "shared").default[Boolean](false),
(js \ "acl").default[ZoneACL](ZoneACL()),
(js \ "adminGroupId").required[String]("Missing Zone.adminGroupId"),
- (js \ "backendId").optional[String]
+ (js \ "backendId").optional[String],
+ (js \ "recurrenceSchedule").optional[String],
+ (js \ "scheduleRequestor").optional[String],
).mapN(CreateZoneInput.apply)
}
@@ -116,7 +136,9 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "shared").default[Boolean](false),
(js \ "acl").default[ZoneACL](ZoneACL()),
(js \ "adminGroupId").required[String]("Missing Zone.adminGroupId"),
- (js \ "backendId").optional[String]
+ (js \ "recurrenceSchedule").optional[String],
+ (js \ "scheduleRequestor").optional[String],
+ (js \ "backendId").optional[String],
).mapN(UpdateZoneInput.apply)
}
@@ -130,18 +152,30 @@ trait DnsJsonProtocol extends JsonValidation {
override def toJson(a: Algorithm): JValue = JString(a.name)
}
+ case object EncryptedSerializer extends ValidationSerializer[Encrypted] {
+ override def fromJson(js: JValue): ValidatedNel[String, Encrypted] =
+ js match {
+ case JString(value) => EncryptFromJson.fromString(value).toValidatedNel
+ case _ => "Unsupported type for zone connection key, must be a string".invalidNel
+ }
+
+ override def toJson(a: Encrypted): JValue = JString(a.value)
+ }
+
case object ZoneConnectionSerializer extends ValidationSerializer[ZoneConnection] {
override def fromJson(js: JValue): ValidatedNel[String, ZoneConnection] =
(
(js \ "name").required[String]("Missing ZoneConnection.name"),
(js \ "keyName").required[String]("Missing ZoneConnection.keyName"),
- (js \ "key").required[String]("Missing ZoneConnection.key"),
+ (js \ "key").required[Encrypted]("Missing ZoneConnection.key"),
(js \ "primaryServer").required[String]("Missing ZoneConnection.primaryServer"),
(js \ "algorithm").default[Algorithm](Algorithm.HMAC_MD5)
).mapN(ZoneConnection.apply)
}
def checkDomainNameLen(s: String): Boolean = s.length <= 255
+ def validateNaptrFlag(flag: String): Boolean = flag == "U" || flag == "S" || flag == "A" || flag == "P"
+ def validateNaptrRegexp(regexp: String): Boolean = regexp.startsWith("!") && regexp.endsWith("!") || regexp == ""
def nameContainsDots(s: String): Boolean = s.contains(".")
def nameDoesNotContainSpaces(s: String): Boolean = !s.contains(" ")
@@ -202,6 +236,7 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "id").default[String](UUID.randomUUID().toString),
(js \ "account").default[String]("system"),
(js \ "ownerGroupId").optional[String],
+ (js \ "recordSetGroupChange").optional[OwnerShipTransfer],
(js \ "fqdn").optional[String]
).mapN(RecordSet.apply)
@@ -226,9 +261,23 @@ trait DnsJsonProtocol extends JsonValidation {
("id" -> rs.id) ~
("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~
+ ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn)
}
+
+ case object ownerShipTransferSerializer extends ValidationSerializer[OwnerShipTransfer] {
+ override def fromJson(js: JValue): ValidatedNel[String, OwnerShipTransfer] =
+ (
+ (js \ "ownerShipTransferStatus").required[OwnerShipTransferStatus]("Missing ownerShipTransfer.ownerShipTransferStatus"),
+ (js \ "requestedOwnerGroupId").optional[String],
+ ).mapN(OwnerShipTransfer.apply)
+
+ override def toJson(rsa: OwnerShipTransfer): JValue =
+ ("ownerShipTransferStatus" -> Extraction.decompose(rsa.ownerShipTransferStatus)) ~
+ ("requestedOwnerGroupId" -> Extraction.decompose(rsa.requestedOwnerGroupId))
+ }
+
case object RecordSetListInfoSerializer extends ValidationSerializer[RecordSetListInfo] {
override def fromJson(js: JValue): ValidatedNel[String, RecordSetListInfo] =
(
@@ -250,6 +299,7 @@ trait DnsJsonProtocol extends JsonValidation {
("accessLevel" -> rs.accessLevel.toString) ~
("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~
+ ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn)
}
@@ -271,6 +321,7 @@ trait DnsJsonProtocol extends JsonValidation {
("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~
+ ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn)
}
@@ -296,6 +347,7 @@ trait DnsJsonProtocol extends JsonValidation {
("account" -> rs.account) ~
("ownerGroupId" -> rs.ownerGroupId) ~
("ownerGroupName" -> rs.ownerGroupName) ~
+ ("recordSetGroupChange" -> Extraction.decompose(rs.recordSetGroupChange)) ~
("fqdn" -> rs.fqdn) ~
("zoneName" -> rs.zoneName) ~
("zoneShared" -> rs.zoneShared)
@@ -487,7 +539,7 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "flags")
.required[String]("Missing NAPTR.flags")
.check(
- "NAPTR.flags must be less than 2 characters" -> (_.length < 2)
+ "Invalid NAPTR.flag. Valid NAPTR flag value must be U, S, A or P" -> validateNaptrFlag
),
(js \ "service")
.required[String]("Missing NAPTR.service")
@@ -497,7 +549,7 @@ trait DnsJsonProtocol extends JsonValidation {
(js \ "regexp")
.required[String]("Missing NAPTR.regexp")
.check(
- "NAPTR.regexp must be less than 255 characters" -> checkDomainNameLen
+ "Invalid NAPTR.regexp. Valid NAPTR regexp value must start and end with '!' or can be empty" -> validateNaptrRegexp
),
(js \ "replacement")
diff --git a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
index 8b4b59509..166d17261 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/MembershipRouting.scala
@@ -49,6 +49,7 @@ class MembershipRoute(
case InvalidGroupError(msg) => complete(StatusCodes.BadRequest, msg)
case UserNotFoundError(msg) => complete(StatusCodes.NotFound, msg)
case InvalidGroupRequestError(msg) => complete(StatusCodes.BadRequest, msg)
+ case EmailValidationError(msg) => complete(StatusCodes.BadRequest, msg)
}
val membershipRoute: Route = path("groups" / Segment) { groupId =>
@@ -161,8 +162,8 @@ class MembershipRoute(
} ~
path("groups" / Segment / "activity") { groupId =>
(get & monitor("Endpoint.groupActivity")) {
- parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
- (startFrom: Option[String], maxItems: Int) =>
+ parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
+ (startFrom: Option[Int], maxItems: Int) =>
handleRejections(invalidQueryHandler) {
validate(
0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
@@ -186,6 +187,13 @@ class MembershipRoute(
}
}
} ~
+ path("groups" / "valid" / "domains") {
+ (get & monitor("Endpoint.validdomains")) {
+ authenticateAndExecute(membershipService.listEmailDomains) { emailDomains =>
+ complete(StatusCodes.OK, emailDomains)
+ }
+ }
+ } ~
path("users" / Segment / "lock") { id =>
(put & monitor("Endpoint.lockUser")) {
authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) {
@@ -204,9 +212,9 @@ class MembershipRoute(
} ~
path("users" / Segment) { id =>
(get & monitor("Endpoint.getUser")) {
- authenticateAndExecute(membershipService.getUser(id, _)) {
+ authenticateAndExecute(membershipService.getUserDetails(id, _)) {
user =>
- complete(StatusCodes.OK, UserResponseInfo(user))
+ complete(StatusCodes.OK, user)
}
}
}
diff --git a/modules/api/src/main/scala/vinyldns/api/route/RecordSetRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/RecordSetRouting.scala
index 72cd04596..99c676813 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/RecordSetRouting.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/RecordSetRouting.scala
@@ -26,7 +26,8 @@ import vinyldns.api.config.LimitsConfig
import vinyldns.api.domain.zone._
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
-import vinyldns.core.domain.record.{NameSort, RecordSet, RecordType}
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
+import vinyldns.core.domain.record.{NameSort, RecordSet, RecordType, RecordTypeSort}
import vinyldns.core.domain.zone.ZoneCommandResult
import scala.concurrent.duration._
@@ -52,7 +53,8 @@ case class ListRecordSetsByZoneResponse(
recordNameFilter: Option[String] = None,
recordTypeFilter: Option[Set[RecordType]] = None,
recordOwnerGroupFilter: Option[String] = None,
- nameSort: NameSort
+ nameSort: NameSort,
+ recordTypeSort: RecordTypeSort
)
class RecordSetRoute(
@@ -100,7 +102,8 @@ class RecordSetRoute(
"recordNameFilter".?,
"recordTypeFilter".?,
"recordOwnerGroupFilter".?,
- "nameSort".as[String].?("ASC")
+ "nameSort".as[String].?("ASC"),
+ "recordTypeSort".as[String].?("None")
) {
(
startFrom: Option[String],
@@ -108,7 +111,8 @@ class RecordSetRoute(
recordNameFilter: Option[String],
recordTypeFilter: Option[String],
recordOwnerGroupFilter: Option[String],
- nameSort: String
+ nameSort: String,
+ recordTypeSort: String
) =>
val convertedRecordTypeFilter = convertRecordTypeFilter(recordTypeFilter)
handleRejections(invalidQueryHandler) {
@@ -126,8 +130,9 @@ class RecordSetRoute(
convertedRecordTypeFilter,
recordOwnerGroupFilter,
NameSort.find(nameSort),
- _
- )
+ _,
+ RecordTypeSort.find(recordTypeSort),
+ )
) { rsResponse =>
complete(StatusCodes.OK, rsResponse)
}
@@ -144,7 +149,8 @@ class RecordSetRoute(
"recordNameFilter".as[String],
"recordTypeFilter".?,
"recordOwnerGroupFilter".?,
- "nameSort".as[String].?("ASC")
+ "nameSort".as[String].?("ASC"),
+ "recordTypeSort".as[String].?("NONE")
) {
(
startFrom: Option[String],
@@ -152,7 +158,8 @@ class RecordSetRoute(
recordNameFilter: String,
recordTypeFilter: Option[String],
recordOwnerGroupFilter: Option[String],
- nameSort: String
+ nameSort: String,
+ recordTypeSort: String
) =>
val convertedRecordTypeFilter = convertRecordTypeFilter(recordTypeFilter)
handleRejections(invalidQueryHandler) {
@@ -169,7 +176,8 @@ class RecordSetRoute(
convertedRecordTypeFilter,
recordOwnerGroupFilter,
NameSort.find(nameSort),
- _
+ _,
+ RecordTypeSort.find(recordTypeSort)
)
) { rsResponse =>
complete(StatusCodes.OK, rsResponse)
@@ -179,6 +187,13 @@ class RecordSetRoute(
}
}
} ~
+ path("zones" / Segment / "recordsetcount") { zoneId =>
+ (get & monitor("Endpoint.getRecordSetCount")) {
+ authenticateAndExecute(recordSetService.getRecordSetCount(zoneId, _)) { count =>
+ complete(StatusCodes.OK, count)
+ }
+ }
+ } ~
path("zones" / Segment / "recordsets" / Segment) { (zoneId, rsId) =>
(get & monitor("Endpoint.getRecordSetByZone")) {
authenticateAndExecute(recordSetService.getRecordSetByZone(rsId, zoneId, _)) { rs =>
@@ -215,8 +230,8 @@ class RecordSetRoute(
} ~
path("zones" / Segment / "recordsetchanges") { zoneId =>
(get & monitor("Endpoint.listRecordSetChanges")) {
- parameters("startFrom".?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
- (startFrom: Option[String], maxItems: Int) =>
+ parameters("startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
+ (startFrom: Option[Int], maxItems: Int) =>
handleRejections(invalidQueryHandler) {
validate(
check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
@@ -233,6 +248,52 @@ class RecordSetRoute(
}
}
}
+ } ~
+ path("recordsetchange" / "history") {
+ (get & monitor("Endpoint.listRecordSetChangeHistory")) {
+ parameters("zoneId".as[String].?, "startFrom".as[Int].?, "maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "fqdn".as[String].?, "recordType".as[String].?) {
+ (zoneId: Option[String], startFrom: Option[Int], maxItems: Int, fqdn: Option[String], recordType: Option[String]) =>
+ handleRejections(invalidQueryHandler) {
+ val errorMessage = if(fqdn.isEmpty || recordType.isEmpty || zoneId.isEmpty) {
+ "recordType, fqdn and zoneId cannot be empty"
+ } else {
+ s"maxItems was $maxItems, maxItems must be between 0 exclusive " +
+ s"and $DEFAULT_MAX_ITEMS inclusive"
+ }
+ val isValid = (0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS) && (fqdn.nonEmpty && recordType.nonEmpty && zoneId.nonEmpty)
+ validate(
+ check = isValid,
+ errorMsg = errorMessage
+ ){
+ authenticateAndExecute(
+ recordSetService
+ .listRecordSetChangeHistory(zoneId, startFrom, maxItems, fqdn, RecordType.find(recordType.get), _)
+ ) { changes =>
+ complete(StatusCodes.OK, changes)
+ }
+ }
+ }
+ }
+ }
+ } ~
+ path("metrics" / "health" / "zones" / Segment / "recordsetchangesfailure") {zoneId =>
+ (get & monitor("Endpoint.listFailedRecordSetChanges")) {
+ parameters("startFrom".as[Int].?(0), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
+ (startFrom: Int, maxItems: Int) =>
+ handleRejections(invalidQueryHandler) {
+ validate(
+ check = 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
+ errorMsg = s"maxItems was $maxItems, maxItems must be between 0 exclusive " +
+ s"and $DEFAULT_MAX_ITEMS inclusive"
+ ){
+ authenticateAndExecute(recordSetService.listFailedRecordSetChanges(_, Some(zoneId), startFrom, maxItems)) {
+ changes =>
+ complete(StatusCodes.OK, changes)
+ }
+ }
+ }
+ }
+ }
}
private val invalidQueryHandler = RejectionHandler
diff --git a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala
index 440f346c3..9cc289b5f 100644
--- a/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala
+++ b/modules/api/src/main/scala/vinyldns/api/route/ZoneRouting.scala
@@ -21,6 +21,7 @@ import akka.http.scaladsl.server._
import akka.util.Timeout
import org.slf4j.{Logger, LoggerFactory}
import vinyldns.api.config.LimitsConfig
+import vinyldns.api.domain.membership.EmailValidationError
import vinyldns.api.domain.zone._
import vinyldns.core.crypto.CryptoAlgebra
import vinyldns.core.domain.zone._
@@ -28,6 +29,7 @@ import vinyldns.core.domain.zone._
import scala.concurrent.duration._
case class GetZoneResponse(zone: ZoneInfo)
+case class GetZoneDetailsResponse(zone: ZoneDetails)
case class ZoneRejected(zone: Zone, errors: List[String])
class ZoneRoute(
@@ -62,6 +64,7 @@ class ZoneRoute(
case RecentSyncError(msg) => complete(StatusCodes.Forbidden, msg)
case ZoneInactiveError(msg) => complete(StatusCodes.BadRequest, msg)
case InvalidRequest(msg) => complete(StatusCodes.BadRequest, msg)
+ case EmailValidationError(msg) => complete(StatusCodes.BadRequest, msg)
}
val zoneRoute: Route = path("zones") {
@@ -79,14 +82,16 @@ class ZoneRoute(
"startFrom".as[String].?,
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
"searchByAdminGroup".as[Boolean].?(false),
- "ignoreAccess".as[Boolean].?(false)
+ "ignoreAccess".as[Boolean].?(false),
+ "includeReverse".as[Boolean].?(true)
) {
(
nameFilter: Option[String],
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean,
- ignoreAccess: Boolean
+ ignoreAccess: Boolean,
+ includeReverse: Boolean
) =>
{
handleRejections(invalidQueryHandler) {
@@ -96,7 +101,7 @@ class ZoneRoute(
) {
authenticateAndExecute(
zoneService
- .listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess)
+ .listZones(_, nameFilter, startFrom, maxItems, searchByAdminGroup, ignoreAccess, includeReverse)
) { result =>
complete(StatusCodes.OK, result)
}
@@ -106,6 +111,38 @@ class ZoneRoute(
}
}
} ~
+ path("zones" / "deleted" / "changes") {
+ (get & monitor("Endpoint.listDeletedZones")) {
+ parameters(
+ "nameFilter".?,
+ "startFrom".as[String].?,
+ "maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
+ "ignoreAccess".as[Boolean].?(false)
+ ) {
+ (
+ nameFilter: Option[String],
+ startFrom: Option[String],
+ maxItems: Int,
+ ignoreAccess: Boolean
+ ) =>
+ {
+ handleRejections(invalidQueryHandler) {
+ validate(
+ 0 < maxItems && maxItems <= MAX_ITEMS_LIMIT,
+ s"maxItems was $maxItems, maxItems must be between 0 and $MAX_ITEMS_LIMIT"
+ ) {
+ authenticateAndExecute(
+ zoneService
+ .listDeletedZones(_, nameFilter, startFrom, maxItems, ignoreAccess)
+ ) { result =>
+ complete(StatusCodes.OK, result)
+ }
+ }
+ }
+ }
+ }
+ }
+ } ~
path("zones" / "backendids") {
(get & monitor("Endpoint.getBackendIds")) {
authenticateAndExecute(_ => zoneService.getBackendIds()) { ids =>
@@ -138,6 +175,13 @@ class ZoneRoute(
}
}
} ~
+ path("zones" / Segment / "details") { id =>
+ (get & monitor("Endpoint.getCommonZoneDetails")) {
+ authenticateAndExecute(zoneService.getCommonZoneDetails(id, _)) { zone =>
+ complete(StatusCodes.OK, GetZoneDetailsResponse(zone))
+ }
+ }
+ } ~
path("zones" / Segment / "sync") { id =>
(post & monitor("Endpoint.syncZone")) {
authenticateAndExecute(zoneService.syncZone(id, _)) { chg =>
@@ -163,6 +207,24 @@ class ZoneRoute(
}
}
} ~
+ path("metrics" / "health" / "zonechangesfailure") {
+ (get & monitor("Endpoint.listFailedZoneChanges")) {
+ parameters("startFrom".as[Int].?(0), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS)) {
+ (startFrom: Int, maxItems: Int) =>
+ handleRejections(invalidQueryHandler) {
+ validate(
+ 0 < maxItems && maxItems <= DEFAULT_MAX_ITEMS,
+ s"maxItems was $maxItems, maxItems must be between 0 exclusive and $DEFAULT_MAX_ITEMS inclusive"
+ ) {
+ authenticateAndExecute(zoneService.listFailedZoneChanges(_, startFrom, maxItems)) {
+ changes =>
+ complete(StatusCodes.OK, changes)
+ }
+ }
+ }
+ }
+ }
+ } ~
path("zones" / Segment / "acl" / "rules") { id =>
(put & monitor("Endpoint.addZoneACLRule")) {
authenticateAndExecuteWithEntity[ZoneCommandResult, ACLRuleInfo](
diff --git a/modules/api/src/test/functional/requirements.txt b/modules/api/src/test/functional/requirements.txt
index decd2c187..1d71a609a 100644
--- a/modules/api/src/test/functional/requirements.txt
+++ b/modules/api/src/test/functional/requirements.txt
@@ -2,11 +2,11 @@ pyhamcrest==2.0.2
pytz>=2014
pytest==6.2.5
mock==4.0.3
-dnspython==2.1.0
+dnspython==2.6.1
boto3==1.18.51
botocore==1.21.51
-requests==2.26.0
+requests==2.32.3
pytest-xdist==2.4.0
python-dateutil==2.8.2
filelock==3.2.0
-pytest-custom_exit_code==0.3.0
\ No newline at end of file
+pytest-custom_exit_code==0.3.0
diff --git a/modules/api/src/test/functional/tests/batch/create_batch_change_test.py b/modules/api/src/test/functional/tests/batch/create_batch_change_test.py
index 341567e4a..102636743 100644
--- a/modules/api/src/test/functional/tests/batch/create_batch_change_test.py
+++ b/modules/api/src/test/functional/tests/batch/create_batch_change_test.py
@@ -14,7 +14,7 @@ def validate_change_error_response_basics(input_json, change_type, input_name, r
assert_that(input_json["changeType"], is_(change_type))
assert_that(input_json["inputName"], is_(input_name))
assert_that(input_json["type"], is_(record_type))
- assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX"]))
+ assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX", "NS", "NAPTR", "SRV"]))
if change_type == "Add":
assert_that(input_json["ttl"], is_(ttl))
if record_type in ["A", "AAAA"]:
@@ -28,6 +28,20 @@ def validate_change_error_response_basics(input_json, change_type, input_name, r
elif record_type == "MX":
assert_that(input_json["record"]["preference"], is_(record_data["preference"]))
assert_that(input_json["record"]["exchange"], is_(record_data["exchange"]))
+ elif record_type == "NS" and change_type == "Add":
+ assert_that(input_json["record"]["nsdname"], is_(record_data))
+ elif record_type == "NAPTR" and change_type == "Add":
+ assert_that(input_json["record"]["order"], is_(record_data["order"]))
+ assert_that(input_json["record"]["preference"], is_(record_data["preference"]))
+ assert_that(input_json["record"]["flags"], is_(record_data["flags"]))
+ assert_that(input_json["record"]["service"], is_(record_data["service"]))
+ assert_that(input_json["record"]["regexp"], is_(record_data["regexp"]))
+ assert_that(input_json["record"]["replacement"], is_(record_data["replacement"]))
+ elif record_type == "SRV" and change_type == "Add":
+ assert_that(input_json["record"]["priority"], is_(record_data["priority"]))
+ assert_that(input_json["record"]["weight"], is_(record_data["weight"]))
+ assert_that(input_json["record"]["port"], is_(record_data["port"]))
+ assert_that(input_json["record"]["target"], is_(record_data["target"]))
return
@@ -56,7 +70,7 @@ def assert_change_success(changes_json, zone, index, record_name, input_name, re
assert_that(changes_json[index]["type"], is_(record_type))
assert_that(changes_json[index]["id"], is_not(none()))
assert_that(changes_json[index]["changeType"], is_(change_type))
- assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX"]))
+ assert_that(record_type, is_in(["A", "AAAA", "CNAME", "PTR", "TXT", "MX", "NS", "NAPTR", "SRV"]))
if record_type in ["A", "AAAA"] and change_type == "Add":
assert_that(changes_json[index]["record"]["address"], is_(record_data))
elif record_type == "CNAME" and change_type == "Add":
@@ -68,6 +82,20 @@ def assert_change_success(changes_json, zone, index, record_name, input_name, re
elif record_type == "MX" and change_type == "Add":
assert_that(changes_json[index]["record"]["preference"], is_(record_data["preference"]))
assert_that(changes_json[index]["record"]["exchange"], is_(record_data["exchange"]))
+ elif record_type == "NS" and change_type == "Add":
+ assert_that(changes_json[index]["record"]["nsdname"], is_(record_data))
+ elif record_type == "NAPTR" and change_type == "Add":
+ assert_that(changes_json[index]["record"]["order"], is_(record_data["order"]))
+ assert_that(changes_json[index]["record"]["preference"], is_(record_data["preference"]))
+ assert_that(changes_json[index]["record"]["flags"], is_(record_data["flags"]))
+ assert_that(changes_json[index]["record"]["service"], is_(record_data["service"]))
+ assert_that(changes_json[index]["record"]["regexp"], is_(record_data["regexp"]))
+ assert_that(changes_json[index]["record"]["replacement"], is_(record_data["replacement"]))
+ elif record_type == "SRV" and change_type == "Add":
+ assert_that(changes_json[index]["record"]["priority"], is_(record_data["priority"]))
+ assert_that(changes_json[index]["record"]["weight"], is_(record_data["weight"]))
+ assert_that(changes_json[index]["record"]["port"], is_(record_data["port"]))
+ assert_that(changes_json[index]["record"]["target"], is_(record_data["target"]))
return
@@ -113,7 +141,9 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
get_change_TXT_json(f"txt-unique-characters.{ok_zone_name}", text='a\\\\`=` =\\"Cat\\"\nattr=val'),
get_change_TXT_json(f"txt.{ip4_zone_name}"),
get_change_MX_json(f"mx.{ok_zone_name}", preference=0),
- get_change_MX_json(f"{ok_zone_name}", preference=1000, exchange="bar.foo.")
+ get_change_MX_json(f"{ok_zone_name}", preference=1000, exchange="bar.foo."),
+ get_change_NAPTR_json(f"naptr.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_SRV_json(f"srv.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo.")
]
}
@@ -161,6 +191,10 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
record_name="mx", input_name=f"mx.{ok_zone_name}", record_data={"preference": 0, "exchange": "foo.bar."}, record_type="MX")
assert_change_success(result["changes"], zone=ok_zone, index=14,
record_name=f"{ok_zone_name}", input_name=f"{ok_zone_name}", record_data={"preference": 1000, "exchange": "bar.foo."}, record_type="MX")
+ assert_change_success(result["changes"], zone=ok_zone, index=15,
+ record_name=f"naptr", input_name=f"naptr.{ok_zone_name}", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, record_type="NAPTR")
+ assert_change_success(result["changes"], zone=ok_zone, index=16,
+ record_name=f"srv", input_name=f"srv.{ok_zone_name}", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, record_type="SRV")
completed_status = [change["status"] == "Complete" for change in completed_batch["changes"]]
assert_that(all(completed_status), is_(True))
@@ -285,6 +319,22 @@ def test_create_batch_change_with_adds_success(shared_zone_test_context):
"ttl": 200,
"records": [{"preference": 1000, "exchange": "bar.foo."}]}
verify_recordset(rs16, expected16)
+
+ rs17 = client.get_recordset(record_set_list[15][0], record_set_list[15][1])["recordSet"]
+ expected17 = {"name": f"naptr",
+ "zoneId": ok_zone["id"],
+ "type": "NAPTR",
+ "ttl": 200,
+ "records": [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}]}
+ verify_recordset(rs17, expected17)
+
+ rs18 = client.get_recordset(record_set_list[16][0], record_set_list[16][1])["recordSet"]
+ expected18 = {"name": f"srv",
+ "zoneId": ok_zone["id"],
+ "type": "SRV",
+ "ttl": 200,
+ "records": [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}]}
+ verify_recordset(rs18, expected18)
finally:
clear_zoneid_rsid_tuple_list(to_delete, client)
@@ -316,6 +366,30 @@ def test_create_batch_change_with_scheduled_time_and_owner_group_succeeds(shared
rejecter.reject_batch_change(result["id"], status=200)
+@pytest.mark.manual_batch_review
+def test_create_batch_change_with_ns_record_goes_to_review(shared_zone_test_context):
+ """
+ Test creating a batch change with ns record goes to review
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ batch_change_input = {
+ "comments": "this is optional",
+ "changes": [
+ get_change_NS_json(f"ns.{ok_zone_name}", nsdname="ns1.parent.com."),
+ ],
+ "ownerGroupId": shared_zone_test_context.ok_group["id"]
+ }
+ result = None
+ try:
+ result = client.create_batch_change(batch_change_input, status=202)
+ assert_that(result["status"], "Pending Review")
+ finally:
+ if result:
+ rejecter = shared_zone_test_context.support_user_client
+ rejecter.reject_batch_change(result["id"], status=200)
+
+
@pytest.mark.manual_batch_review
def test_create_scheduled_batch_change_with_zone_discovery_error_without_owner_group_fails(shared_zone_test_context):
"""
@@ -890,7 +964,7 @@ def test_create_batch_change_with_unsupported_record_type_fails(shared_zone_test
errors = client.create_batch_change(batch_change_input, status=400)
assert_error(errors,
- error_messages=["Unsupported type UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, and MX"])
+ error_messages=["Unsupported type UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, MX, NS, SRV and NAPTR"])
def test_create_batch_change_with_high_value_domain_fails(shared_zone_test_context):
@@ -1481,8 +1555,8 @@ def test_a_recordtype_add_checks(shared_zone_test_context):
# context validations: conflicting recordsets, unauthorized error
assert_failed_change_in_error_response(response[7], input_name=existing_a_fqdn, record_data="1.2.3.4",
- error_messages=[f"RecordName \"{existing_a_fqdn}\" already exists. Your request will be manually reviewed. "
- f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add."])
+ error_messages=[f"RecordName \"{existing_a_fqdn}\" already exists. "
+ f"If you intended to update this record, submit a DeleteRecordSet entry followed by an Add."])
assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn,
record_data="1.2.3.4",
error_messages=[f'CNAME Conflict: CNAME record names must be unique. '
@@ -1938,7 +2012,8 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
get_change_CNAME_json(existing_forward_fqdn),
get_change_CNAME_json(existing_cname_fqdn),
get_change_CNAME_json(f"0.{ip4_zone_name}", cname="duplicate.in.db."),
- get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}")
+ get_change_CNAME_json(f"user-add-unauthorized.{dummy_zone_name}"),
+ get_change_CNAME_json(f"invalid-ipv4-{parent_zone_name}", cname="1.2.3.4")
]
}
@@ -2003,8 +2078,8 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
f"Existing record with name \"{existing_forward_fqdn}\" and type \"A\" conflicts with this record."])
assert_failed_change_in_error_response(response[14], input_name=existing_cname_fqdn,
record_type="CNAME", record_data="test.com.",
- error_messages=[f"RecordName \"{existing_cname_fqdn}\" already exists. Your request will be manually reviewed. "
- f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add.",
+ error_messages=[f"RecordName \"{existing_cname_fqdn}\" already exists. "
+ f"If you intended to update this record, submit a DeleteRecordSet entry followed by an Add.",
f"CNAME Conflict: CNAME record names must be unique. "
f"Existing record with name \"{existing_cname_fqdn}\" and type \"CNAME\" conflicts with this record."])
assert_failed_change_in_error_response(response[15], input_name=existing_reverse_fqdn, record_type="CNAME",
@@ -2014,6 +2089,9 @@ def test_cname_recordtype_add_checks(shared_zone_test_context):
assert_failed_change_in_error_response(response[16], input_name=f"user-add-unauthorized.{dummy_zone_name}",
record_type="CNAME", record_data="test.com.",
error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[17], input_name=f"invalid-ipv4-{parent_zone_name}", record_type="CNAME", record_data="1.2.3.4.",
+ error_messages=[f'Invalid Cname: "Fqdn(1.2.3.4.)", Valid CNAME record data should not be an IP address'])
+
finally:
clear_recordset_list(to_delete, client)
@@ -2291,9 +2369,9 @@ def test_ipv4_ptr_recordtype_add_checks(shared_zone_test_context):
# delegated and non-delegated PTR duplicate name checks
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.196", record_type="PTR", record_data="test.com.")
- assert_successful_change_in_error_response(response[5], input_name=f"196.{ip4_zone_name}", record_type="CNAME", record_data="test.com.")
- assert_failed_change_in_error_response(response[6], input_name=f"196.192/30.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
- error_messages=[f'Record Name "196.192/30.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
+ assert_failed_change_in_error_response(response[5], input_name=f"196.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
+ error_messages=[f'Record Name "196.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
+ assert_successful_change_in_error_response(response[6], input_name=f"196.192/30.{ip4_zone_name}", record_type="CNAME", record_data="test.com.")
assert_successful_change_in_error_response(response[7], input_name=f"{ip4_prefix}.55", record_type="PTR", record_data="test.com.")
assert_failed_change_in_error_response(response[8], input_name=f"55.{ip4_zone_name}", record_type="CNAME", record_data="test.com.",
error_messages=[f'Record Name "55.{ip4_zone_name}" Not Unique In Batch Change: cannot have multiple "CNAME" records with the same name.'])
@@ -2306,8 +2384,8 @@ def test_ipv4_ptr_recordtype_add_checks(shared_zone_test_context):
# context validations: existing cname recordset
assert_failed_change_in_error_response(response[11], input_name=f"{ip4_prefix}.193", record_type="PTR", record_data="existing-ptr.",
- error_messages=[f'RecordName "{ip4_prefix}.193" already exists. Your request will be manually reviewed. '
- f'If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add.'])
+ error_messages=[f'RecordName "{ip4_prefix}.193" already exists. '
+ f'If you intended to update this record, submit a DeleteRecordSet entry followed by an Add.'])
assert_failed_change_in_error_response(response[12], input_name=f"{ip4_prefix}.199", record_type="PTR", record_data="existing-cname.",
error_messages=[
f'CNAME Conflict: CNAME record names must be unique. Existing record with name "{ip4_prefix}.199" and type "CNAME" conflicts with this record.'])
@@ -2516,8 +2594,8 @@ def test_ipv6_ptr_recordtype_add_checks(shared_zone_test_context):
# context validations: existing record sets pre-request
assert_failed_change_in_error_response(response[5], input_name=f"{ip6_prefix}:1000::bbbb", record_type="PTR",
record_data="existing.ptr.",
- error_messages=[f"RecordName \"{ip6_prefix}:1000::bbbb\" already exists. Your request will be manually reviewed. "
- f"If you intended to update this record, you can avoid manual review by adding a DeleteRecordSet entry followed by an Add."])
+ error_messages=[f"RecordName \"{ip6_prefix}:1000::bbbb\" already exists. "
+ f"If you intended to update this record, submit a DeleteRecordSet entry followed by an Add."])
finally:
clear_recordset_list(to_delete, client)
@@ -3725,7 +3803,7 @@ def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_t
ok_zone_name = shared_zone_test_context.ok_zone["name"]
batch_change_input = {
- "comments": "test delete record failures",
+ "comments": "test delete record",
"changes": [
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet")
]
@@ -3734,12 +3812,46 @@ def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_t
response = client.create_batch_change(batch_change_input, status=202)
get_batch = client.get_batch_change(response["id"])
- assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist." +
+ assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist. " +
"No further action is required."))
assert_successful_change_in_error_response(response["changes"][0], input_name=f"delete-non-existent-record.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet")
+def test_create_batch_delete_record_data_that_does_not_exists_completes(shared_zone_test_context):
+ """
+ Test delete record set completes for non-existent record data
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ batch_change_input = {
+ "comments": "this is optional",
+ "changes": [
+ get_change_A_AAAA_json(f"delete-non-existent-record-data.{ok_zone_name}", address="4.5.6.7"),
+ ]
+ }
+ batch_change_delete_input = {
+ "comments": "test delete record",
+ "changes": [
+ get_change_A_AAAA_json(f"delete-non-existent-record-data.{ok_zone_name}", address="1.1.1.1", change_type="DeleteRecordSet")
+ ]
+ }
+
+ to_delete = []
+ try:
+ result = client.create_batch_change(batch_change_input, status=202)
+ completed_batch = client.wait_until_batch_change_completed(result)
+ record_set_list = [(change["zoneId"], change["recordSetId"]) for change in completed_batch["changes"]]
+ to_delete = set(record_set_list)
+ response = client.create_batch_change(batch_change_delete_input, status=202)
+ get_batch = client.get_batch_change(response["id"])
+ assert_that(get_batch["changes"][0]["systemMessage"], is_("Record data entered does not exist. " +
+ "No further action is required."))
+ assert_successful_change_in_error_response(response["changes"][0], input_name=f"delete-non-existent-record-data.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet")
+ finally:
+ clear_zoneid_rsid_tuple_list(to_delete, client)
+
+
@pytest.mark.serial
def test_create_batch_delete_record_access_checks(shared_zone_test_context):
"""
@@ -4109,7 +4221,7 @@ def test_create_batch_deletes_succeeds(shared_zone_test_context):
@pytest.mark.skip_production
def test_create_batch_change_with_multi_record_adds_with_multi_record_support(shared_zone_test_context):
"""
- Test new recordsets with multiple records can be added in batch, but existing recordsets cannot be added to
+ Test new recordsets with multiple records can be added in batch.
"""
client = shared_zone_test_context.ok_vinyldns_client
ok_zone = shared_zone_test_context.ok_zone
@@ -4134,7 +4246,7 @@ def test_create_batch_change_with_multi_record_adds_with_multi_record_support(sh
get_change_TXT_json(f"multi-txt.{ok_zone_name}", text="more-multi-text"),
get_change_MX_json(f"multi-mx.{ok_zone_name}", preference=0),
get_change_MX_json(f"multi-mx.{ok_zone_name}", preference=1000, exchange="bar.foo."),
- get_change_A_AAAA_json(rs_fqdn, address="1.1.1.1")
+ get_change_A_AAAA_json(rs_fqdn, address="1.2.3.4")
],
"ownerGroupId": shared_zone_test_context.ok_group["id"]
}
@@ -4152,6 +4264,637 @@ def test_create_batch_change_with_multi_record_adds_with_multi_record_support(sh
assert_successful_change_in_error_response(response["changes"][5], input_name=f"multi-txt.{ok_zone_name}", record_type="TXT", record_data="more-multi-text")
assert_successful_change_in_error_response(response["changes"][6], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 0, "exchange": "foo.bar."})
assert_successful_change_in_error_response(response["changes"][7], input_name=f"multi-mx.{ok_zone_name}", record_type="MX", record_data={"preference": 1000, "exchange": "bar.foo."})
- assert_successful_change_in_error_response(response["changes"][8], input_name=rs_fqdn, record_data="1.1.1.1")
+ assert_successful_change_in_error_response(response["changes"][8], input_name=rs_fqdn, record_data="1.2.3.4")
finally:
clear_recordset_list(to_delete, client)
+
+
+def test_ns_recordtype_add_checks(shared_zone_test_context):
+ """
+ Test all add validations performed on NS records submitted in batch changes
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+
+ existing_ns_name = generate_record_name()
+ existing_ns_fqdn = existing_ns_name + f".{ok_zone_name}"
+ existing_ns = create_recordset(shared_zone_test_context.ok_zone, existing_ns_name, "NS", [{"nsdname": "ns1.parent.com."}], 100)
+
+ existing_cname_name = generate_record_name()
+ existing_cname_fqdn = existing_cname_name + f".{ok_zone_name}"
+ existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME",
+ [{"cname": "test."}], 100)
+
+ batch_change_input = {
+ "changes": [
+ # valid change
+ get_change_NS_json(f"ns.{ok_zone_name}", nsdname="ns1.parent.com."),
+
+ # input validation failures
+ get_change_NS_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, nsdname="ns1.parent.com."),
+
+ # zone discovery failures
+ get_change_NS_json(f"no.zone.at.all.", nsdname="ns1.parent.com."),
+
+ # context validation failures
+ get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
+ get_change_NS_json(f"cname-duplicate.{ok_zone_name}", nsdname="ns1.parent.com."),
+ get_change_NS_json(existing_ns_fqdn, nsdname="ns1.parent.com."),
+ get_change_NS_json(existing_cname_fqdn, nsdname="ns1.parent.com."),
+ get_change_NS_json(f"unapproved.{ok_zone_name}", nsdname="unapproved.name.server."),
+ get_change_NS_json(f"user-add-unauthorized.{dummy_zone_name}", nsdname="ns1.parent.com.")
+ ]
+ }
+
+ to_create = [existing_ns, existing_cname]
+ to_delete = []
+ try:
+ for create_json in to_create:
+ create_result = client.create_recordset(create_json, status=202)
+ to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
+
+ response = client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=f"ns.{ok_zone_name}", record_type="NS",
+ record_data="ns1.parent.com.")
+
+ # ttl, domain name, record data
+ assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29,
+ record_type="NS", record_data="ns1.parent.com.",
+ error_messages=[
+ 'Invalid TTL: "29", must be a number between 30 and 2147483647.',
+ f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_name}", '
+ "valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot."])
+
+ # zone discovery failure
+ assert_failed_change_in_error_response(response[2], input_name="no.zone.at.all.", record_type="NS",
+ record_data="ns1.parent.com.",
+ error_messages=['Zone Discovery Failed: zone for "no.zone.at.all." does not exist in VinylDNS. '
+ 'If zone exists, then it must be connected to in VinylDNS.'])
+
+ # context validations: cname duplicate
+ assert_failed_change_in_error_response(response[3], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
+ record_data="test.com.",
+ error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
+ f"cannot have multiple \"CNAME\" records with the same name."])
+
+ # context validations: conflicting recordsets, unauthorized error
+ assert_successful_change_in_error_response(response[5], input_name=existing_ns_fqdn, record_type="NS",
+ record_data="ns1.parent.com.")
+ assert_failed_change_in_error_response(response[6], input_name=existing_cname_fqdn, record_type="NS",
+ record_data="ns1.parent.com.",
+ error_messages=[f"CNAME Conflict: CNAME record names must be unique. "
+ f"Existing record with name \"{existing_cname_fqdn}\" and type \"CNAME\" conflicts with this record."])
+ assert_failed_change_in_error_response(response[7], input_name=f"unapproved.{ok_zone_name}",
+ record_type="NS", record_data="unapproved.name.server.",
+ error_messages=[f"Name Server unapproved.name.server. is not an approved name server."])
+ assert_failed_change_in_error_response(response[8], input_name=f"user-add-unauthorized.{dummy_zone_name}",
+ record_type="NS", record_data="ns1.parent.com.",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ clear_recordset_list(to_delete, client)
+
+
+def test_ns_recordtype_update_delete_checks(shared_zone_test_context):
+ """
+ Test all update and delete validations performed on NS records submitted in batch changes
+ """
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ ok_zone = shared_zone_test_context.ok_zone
+ dummy_zone = shared_zone_test_context.dummy_zone
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+
+ rs_delete_name = generate_record_name()
+ rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
+ rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
+
+ rs_update_name = generate_record_name()
+ rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
+ rs_update_ok = create_recordset(ok_zone, rs_update_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
+
+ rs_delete_dummy_name = generate_record_name()
+ rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
+ rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
+
+ rs_update_dummy_name = generate_record_name()
+ rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
+ rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "NS", [{"nsdname": "ns1.parent.com."}], 200)
+
+ batch_change_input = {
+ "comments": "this is optional",
+ "changes": [
+ # valid changes
+ get_change_NS_json(rs_delete_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+ get_change_NS_json(rs_update_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+ get_change_NS_json(rs_update_fqdn, ttl=300, nsdname="ns1.parent.com."),
+ get_change_NS_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+ get_change_NS_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+
+ # input validations failures
+ get_change_NS_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+ get_change_NS_json(f"invalid-ttl.{ok_zone_name}", ttl=29, nsdname="ns1.parent.com."),
+
+ # zone discovery failure
+ get_change_NS_json("no.zone.at.all.", change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+
+ # context validation failures
+ get_change_NS_json(f"update-nonexistent.{ok_zone_name}", nsdname="ns1.parent.com."),
+ get_change_NS_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com."),
+ get_change_NS_json(rs_update_dummy_fqdn, nsdname="ns1.parent.com."),
+ get_change_NS_json(f"unapproved.{ok_zone_name}", nsdname="unapproved.name.server."),
+ get_change_NS_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", nsdname="ns1.parent.com.")
+ ]
+ }
+
+ to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
+ to_delete = []
+
+ try:
+ for rs in to_create:
+ if rs["zoneId"] == dummy_zone["id"]:
+ create_client = dummy_client
+ else:
+ create_client = ok_client
+
+ create_rs = create_client.create_recordset(rs, status=202)
+ to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
+
+ # Confirm that record set doesn't already exist
+ ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
+
+ response = ok_client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="NS", record_data="ns1.parent.com.")
+ assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet")
+
+ # input validations failures: invalid input name, reverse zone error, invalid ttl
+ assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet",
+ error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be '
+ f'letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
+ assert_failed_change_in_error_response(response[6], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="NS", record_data="ns1.parent.com.",
+ error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
+
+ # zone discovery failure
+ assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet",
+ error_messages=[
+ "Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
+ "If zone exists, then it must be connected to in VinylDNS."])
+
+ # context validation failures: record does not exist, not authorized
+ assert_successful_change_in_error_response(response[8], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NS", record_data="ns1.parent.com.")
+ assert_failed_change_in_error_response(response[9], input_name=rs_delete_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[10], input_name=rs_update_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[11], input_name=f"unapproved.{ok_zone_name}",
+ record_type="NS", record_data="unapproved.name.server.",
+ error_messages=[f"Name Server unapproved.name.server. is not an approved name server."])
+ assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="NS", record_data="ns1.parent.com.", change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ # Clean up updates
+ dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
+ ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
+ clear_recordset_list(dummy_deletes, dummy_client)
+ clear_recordset_list(ok_deletes, ok_client)
+
+
+def test_naptr_recordtype_add_checks(shared_zone_test_context):
+ """
+ Test all add validations performed on NAPTR records submitted in batch changes
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+ ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
+
+ existing_naptr_name = generate_record_name()
+ existing_naptr_fqdn = f"{existing_naptr_name}.{ok_zone_name}"
+ existing_naptr = create_recordset(shared_zone_test_context.ok_zone, existing_naptr_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 100)
+
+ existing_cname_name = generate_record_name()
+ existing_cname_fqdn = f"{existing_cname_name}.{ok_zone_name}"
+ existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME", [{"cname": "test."}], 100)
+
+ good_record_fqdn = generate_record_name(ok_zone_name)
+ batch_change_input = {
+ "changes": [
+ # valid change
+ get_change_NAPTR_json(good_record_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # input validation failures
+ get_change_NAPTR_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"naptr.{ip4_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # zone discovery failures
+ get_change_NAPTR_json(f"no.subzone.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json("no.zone.at.all.", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # context validation failures
+ get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
+ get_change_NAPTR_json(f"cname-duplicate.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(existing_naptr_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(existing_cname_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"user-add-unauthorized.{dummy_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns.")
+ ]
+ }
+
+ to_create = [existing_naptr, existing_cname]
+ to_delete = []
+ try:
+ for create_json in to_create:
+ create_result = client.create_recordset(create_json, status=202)
+ to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
+
+ response = client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=good_record_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
+
+ # ttl, domain name, record data
+ assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
+ f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_name}", '
+ "valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot."])
+ assert_failed_change_in_error_response(response[2], input_name=f"naptr.{ip4_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=[f'Invalid Record Type In Reverse Zone: record with name "naptr.{ip4_zone_name}" and type "NAPTR" is not allowed in a reverse zone.'])
+
+ # zone discovery failures
+ assert_failed_change_in_error_response(response[3], input_name=f"no.subzone.{ok_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=[f'Zone Discovery Failed: zone for "no.subzone.{ok_zone_name}" does not exist in VinylDNS. '
+ f'If zone exists, then it must be connected to in VinylDNS.'])
+ assert_failed_change_in_error_response(response[4], input_name="no.zone.at.all.", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=['Zone Discovery Failed: zone for "no.zone.at.all." does not exist in VinylDNS. '
+ 'If zone exists, then it must be connected to in VinylDNS.'])
+
+ # context validations: cname duplicate
+ assert_failed_change_in_error_response(response[5], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
+ record_data="test.com.",
+ error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
+ f"cannot have multiple \"CNAME\" records with the same name."])
+
+ # context validations: conflicting recordsets, unauthorized error
+ assert_successful_change_in_error_response(response[7], input_name=existing_naptr_fqdn, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
+ assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=["CNAME Conflict: CNAME record names must be unique. "
+ f"Existing record with name \"{existing_cname_fqdn}\" and type \"CNAME\" conflicts with this record."])
+ assert_failed_change_in_error_response(response[9], input_name=f"user-add-unauthorized.{dummy_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ clear_recordset_list(to_delete, client)
+
+
+def test_naptr_recordtype_update_delete_checks(shared_zone_test_context):
+ """
+ Test all update and delete validations performed on NAPTR records submitted in batch changes
+ """
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ ok_zone = shared_zone_test_context.ok_zone
+ dummy_zone = shared_zone_test_context.dummy_zone
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
+
+ rs_delete_name = generate_record_name()
+ rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
+ rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
+
+ rs_update_name = generate_record_name()
+ rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
+ rs_update_ok = create_recordset(ok_zone, rs_update_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
+
+ rs_delete_dummy_name = generate_record_name()
+ rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
+ rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
+
+ rs_update_dummy_name = generate_record_name()
+ rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
+ rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "NAPTR", [{"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}], 200)
+
+ batch_change_input = {
+ "comments": "this is optional",
+ "changes": [
+ # valid changes
+ get_change_NAPTR_json(rs_delete_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(rs_update_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(rs_update_fqdn, ttl=300, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # input validations failures
+ get_change_NAPTR_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"delete.{ok_zone_name}", ttl=29, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(f"naptr.{ip4_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # zone discovery failures
+ get_change_NAPTR_json("no.zone.at.all.", change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+
+ # context validation failures
+ get_change_NAPTR_json(f"update-nonexistent.{ok_zone_name}", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(rs_update_dummy_fqdn, order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns."),
+ get_change_NAPTR_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", order=1, preference=1000, flags="U", service="E2U+sip", regexp="!.*!test.!", replacement="target.vinyldns.")
+ ]
+ }
+
+ to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
+ to_delete = []
+
+ try:
+ for rs in to_create:
+ if rs["zoneId"] == dummy_zone["id"]:
+ create_client = dummy_client
+ else:
+ create_client = ok_client
+
+ create_rs = create_client.create_recordset(rs, status=202)
+ to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
+
+ # Confirm that record set doesn't already exist
+ ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
+
+ response = ok_client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
+ assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet")
+
+ # input validations failures: invalid input name, reverse zone error, invalid ttl
+ assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="NAPTR", record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ change_type="DeleteRecordSet",
+ error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be letters, '
+ f'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
+ assert_failed_change_in_error_response(response[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
+ assert_failed_change_in_error_response(response[7], input_name=f"naptr.{ip4_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=[f'Invalid Record Type In Reverse Zone: record with name "naptr.{ip4_zone_name}" '
+ f'and type "NAPTR" is not allowed in a reverse zone.'])
+
+ # zone discovery failure
+ assert_failed_change_in_error_response(response[8], input_name="no.zone.at.all.", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet",
+ error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
+ "If zone exists, then it must be connected to in VinylDNS."])
+
+ # context validation failures: record does not exist, not authorized
+ assert_successful_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."})
+ assert_failed_change_in_error_response(response[10], input_name=rs_delete_dummy_fqdn, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[11], input_name=rs_update_dummy_fqdn, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."},
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="NAPTR",
+ record_data={"order": 1, "preference": 1000, "flags": "U", "service": "E2U+sip", "regexp": "!.*!test.!", "replacement": "target.vinyldns."}, change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ # Clean up updates
+ dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
+ ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
+ clear_recordset_list(dummy_deletes, dummy_client)
+ clear_recordset_list(ok_deletes, ok_client)
+
+
+def test_srv_recordtype_add_checks(shared_zone_test_context):
+ """
+ Test all add validations performed on SRV records submitted in batch changes
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+ ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
+
+ existing_srv_name = generate_record_name()
+ existing_srv_fqdn = f"{existing_srv_name}.{ok_zone_name}"
+ existing_srv = create_recordset(shared_zone_test_context.ok_zone, existing_srv_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 100)
+
+ existing_cname_name = generate_record_name()
+ existing_cname_fqdn = f"{existing_cname_name}.{ok_zone_name}"
+ existing_cname = create_recordset(shared_zone_test_context.ok_zone, existing_cname_name, "CNAME", [{"cname": "test."}], 100)
+
+ good_record_fqdn = generate_record_name(ok_zone_name)
+ batch_change_input = {
+ "changes": [
+ # valid change
+ get_change_SRV_json(good_record_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # input validation failures
+ get_change_SRV_json(f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"srv.{ip4_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # zone discovery failures
+ get_change_SRV_json(f"no.subzone.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json("no.zone.at.all.", priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # context validation failures
+ get_change_CNAME_json(f"cname-duplicate.{ok_zone_name}"),
+ get_change_SRV_json(f"cname-duplicate.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(existing_srv_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(existing_cname_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"user-add-unauthorized.{dummy_zone_name}", priority=1000, weight=5, port=20, target="bar.foo.")
+ ]
+ }
+
+ to_create = [existing_srv, existing_cname]
+ to_delete = []
+ try:
+ for create_json in to_create:
+ create_result = client.create_recordset(create_json, status=202)
+ to_delete.append(client.wait_until_recordset_change_status(create_result, "Complete"))
+
+ response = client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=good_record_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
+
+ # ttl, domain name, record data
+ assert_failed_change_in_error_response(response[1], input_name=f"bad-ttl-and-invalid-name$.{ok_zone_name}", ttl=29, record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
+ f'Invalid domain name: "bad-ttl-and-invalid-name$.{ok_zone_name}", '
+ "valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot."])
+ assert_failed_change_in_error_response(response[2], input_name=f"srv.{ip4_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=[f'Invalid Record Type In Reverse Zone: record with name "srv.{ip4_zone_name}" and type "SRV" is not allowed in a reverse zone.'])
+
+ # zone discovery failures
+ assert_failed_change_in_error_response(response[3], input_name=f"no.subzone.{ok_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=[f'Zone Discovery Failed: zone for "no.subzone.{ok_zone_name}" does not exist in VinylDNS. '
+ f'If zone exists, then it must be connected to in VinylDNS.'])
+ assert_failed_change_in_error_response(response[4], input_name="no.zone.at.all.", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=['Zone Discovery Failed: zone for "no.zone.at.all." does not exist in VinylDNS. '
+ 'If zone exists, then it must be connected to in VinylDNS.'])
+
+ # context validations: cname duplicate
+ assert_failed_change_in_error_response(response[5], input_name=f"cname-duplicate.{ok_zone_name}", record_type="CNAME",
+ record_data="test.com.",
+ error_messages=[f"Record Name \"cname-duplicate.{ok_zone_name}\" Not Unique In Batch Change: "
+ f"cannot have multiple \"CNAME\" records with the same name."])
+
+ # context validations: conflicting recordsets, unauthorized error
+ assert_successful_change_in_error_response(response[7], input_name=existing_srv_fqdn, record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
+ assert_failed_change_in_error_response(response[8], input_name=existing_cname_fqdn, record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=["CNAME Conflict: CNAME record names must be unique. "
+ f"Existing record with name \"{existing_cname_fqdn}\" and type \"CNAME\" conflicts with this record."])
+ assert_failed_change_in_error_response(response[9], input_name=f"user-add-unauthorized.{dummy_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ clear_recordset_list(to_delete, client)
+
+
+def test_srv_recordtype_update_delete_checks(shared_zone_test_context):
+ """
+ Test all update and delete validations performed on SRV records submitted in batch changes
+ """
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ ok_zone = shared_zone_test_context.ok_zone
+ dummy_zone = shared_zone_test_context.dummy_zone
+ dummy_zone_name = shared_zone_test_context.dummy_zone["name"]
+
+ dummy_group_name = shared_zone_test_context.dummy_group["name"]
+ ok_zone_name = shared_zone_test_context.ok_zone["name"]
+ ip4_zone_name = shared_zone_test_context.classless_base_zone["name"]
+
+ rs_delete_name = generate_record_name()
+ rs_delete_fqdn = rs_delete_name + f".{ok_zone_name}"
+ rs_delete_ok = create_recordset(ok_zone, rs_delete_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
+
+ rs_update_name = generate_record_name()
+ rs_update_fqdn = rs_update_name + f".{ok_zone_name}"
+ rs_update_ok = create_recordset(ok_zone, rs_update_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
+
+ rs_delete_dummy_name = generate_record_name()
+ rs_delete_dummy_fqdn = rs_delete_dummy_name + f".{dummy_zone_name}"
+ rs_delete_dummy = create_recordset(dummy_zone, rs_delete_dummy_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
+
+ rs_update_dummy_name = generate_record_name()
+ rs_update_dummy_fqdn = rs_update_dummy_name + f".{dummy_zone_name}"
+ rs_update_dummy = create_recordset(dummy_zone, rs_update_dummy_name, "SRV", [{"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}], 200)
+
+ batch_change_input = {
+ "comments": "this is optional",
+ "changes": [
+ # valid changes
+ get_change_SRV_json(rs_delete_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(rs_update_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(rs_update_fqdn, ttl=300, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # input validations failures
+ get_change_SRV_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"delete.{ok_zone_name}", ttl=29, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(f"srv.{ip4_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # zone discovery failures
+ get_change_SRV_json("no.zone.at.all.", change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+
+ # context validation failures
+ get_change_SRV_json(f"update-nonexistent.{ok_zone_name}", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(rs_update_dummy_fqdn, priority=1000, weight=5, port=20, target="bar.foo."),
+ get_change_SRV_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet", priority=1000, weight=5, port=20, target="bar.foo.")
+ ]
+ }
+
+ to_create = [rs_delete_ok, rs_update_ok, rs_delete_dummy, rs_update_dummy]
+ to_delete = []
+
+ try:
+ for rs in to_create:
+ if rs["zoneId"] == dummy_zone["id"]:
+ create_client = dummy_client
+ else:
+ create_client = ok_client
+
+ create_rs = create_client.create_recordset(rs, status=202)
+ to_delete.append(create_client.wait_until_recordset_change_status(create_rs, "Complete"))
+
+ # Confirm that record set doesn't already exist
+ ok_client.get_recordset(ok_zone["id"], "delete-nonexistent", status=404)
+
+ response = ok_client.create_batch_change(batch_change_input, status=400)
+
+ # successful changes
+ assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
+ assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
+ assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet")
+
+ # input validations failures: invalid input name, reverse zone error, invalid ttl
+ assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="SRV", record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ change_type="DeleteRecordSet",
+ error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be letters, '
+ f'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
+ assert_failed_change_in_error_response(response[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
+ assert_failed_change_in_error_response(response[7], input_name=f"srv.{ip4_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=[f'Invalid Record Type In Reverse Zone: record with name "srv.{ip4_zone_name}" '
+ f'and type "SRV" is not allowed in a reverse zone.'])
+
+ # zone discovery failure
+ assert_failed_change_in_error_response(response[8], input_name="no.zone.at.all.", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."}, change_type="DeleteRecordSet",
+ error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
+ "If zone exists, then it must be connected to in VinylDNS."])
+
+ # context validation failures: record does not exist, not authorized
+ assert_successful_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."})
+ assert_failed_change_in_error_response(response[10], input_name=rs_delete_dummy_fqdn, record_type="SRV",
+ record_data=None, change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[11], input_name=rs_update_dummy_fqdn, record_type="SRV",
+ record_data={"priority": 1000, "weight": 5, "port": 20, "target": "bar.foo."},
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ assert_failed_change_in_error_response(response[12], input_name=rs_update_dummy_fqdn, record_type="SRV",
+ record_data=None, change_type="DeleteRecordSet",
+ error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
+ finally:
+ # Clean up updates
+ dummy_deletes = [rs for rs in to_delete if rs["zone"]["id"] == dummy_zone["id"]]
+ ok_deletes = [rs for rs in to_delete if rs["zone"]["id"] != dummy_zone["id"]]
+ clear_recordset_list(dummy_deletes, dummy_client)
+ clear_recordset_list(ok_deletes, ok_client)
+
diff --git a/modules/api/src/test/functional/tests/internal/status_test.py b/modules/api/src/test/functional/tests/internal/status_test.py
index c6cfccc5a..327ab3301 100644
--- a/modules/api/src/test/functional/tests/internal/status_test.py
+++ b/modules/api/src/test/functional/tests/internal/status_test.py
@@ -77,7 +77,7 @@ def test_toggle_processing(shared_zone_test_context):
# Create changes to make sure we can process after the toggle
# attempt to perform an update
- ok_zone["email"] = "foo@bar.com"
+ ok_zone["email"] = "test@test.com"
zone_change_result = client.update_zone(ok_zone, status=202)
# attempt to a create a record
diff --git a/modules/api/src/test/functional/tests/membership/create_group_test.py b/modules/api/src/test/functional/tests/membership/create_group_test.py
index 0fcaaa95f..b3662dbb3 100644
--- a/modules/api/src/test/functional/tests/membership/create_group_test.py
+++ b/modules/api/src/test/functional/tests/membership/create_group_test.py
@@ -10,7 +10,7 @@ def test_create_group_success(shared_zone_test_context):
try:
new_group = {
- "name": f"test-create-group-success{shared_zone_test_context.partition_id}",
+ "name": "test-create-group-success{shared_zone_test_context.partition_id}",
"email": "test@test.com",
"description": "this is a description",
"members": [{"id": "ok"}],
@@ -32,6 +32,67 @@ def test_create_group_success(shared_zone_test_context):
if result:
client.delete_group(result["id"], status=(200, 404))
+def test_create_group_success_wildcard(shared_zone_test_context):
+ """
+ Tests that creating a group works
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result = None
+
+ try:
+ new_group = {
+ "name": "test-create-group-success_wildcard{shared_zone_test_context.partition_id}",
+ "email": "test@ok.dummy.com",
+ "description": "this is a description",
+ "members": [{"id": "ok"}],
+ "admins": [{"id": "ok"}]
+ }
+ result = client.create_group(new_group, status=200)
+
+ assert_that(result["name"], is_(new_group["name"]))
+ assert_that(result["email"], is_(new_group["email"]))
+ assert_that(result["description"], is_(new_group["description"]))
+ assert_that(result["status"], is_("Active"))
+ assert_that(result["created"], not_none())
+ assert_that(result["id"], not_none())
+ assert_that(result["members"], has_length(1))
+ assert_that(result["members"][0]["id"], is_("ok"))
+ assert_that(result["admins"], has_length(1))
+ assert_that(result["admins"][0]["id"], is_("ok"))
+ finally:
+ if result:
+ client.delete_group(result["id"], status=(200, 404))
+
+def test_create_group_success_number_of_dots(shared_zone_test_context):
+ """
+ Tests that creating a group works
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result = None
+
+ try:
+ new_group = {
+ "name": "test-create-group-success_wildcard{shared_zone_test_context.partition_id}",
+ "email": "test@ok.dummy.com",
+ "description": "this is a description",
+ "members": [{"id": "ok"}],
+ "admins": [{"id": "ok"}]
+ }
+ result = client.create_group(new_group, status=200)
+
+ assert_that(result["name"], is_(new_group["name"]))
+ assert_that(result["email"], is_(new_group["email"]))
+ assert_that(result["description"], is_(new_group["description"]))
+ assert_that(result["status"], is_("Active"))
+ assert_that(result["created"], not_none())
+ assert_that(result["id"], not_none())
+ assert_that(result["members"], has_length(1))
+ assert_that(result["members"][0]["id"], is_("ok"))
+ assert_that(result["admins"], has_length(1))
+ assert_that(result["admins"][0]["id"], is_("ok"))
+ finally:
+ if result:
+ client.delete_group(result["id"], status=(200, 404))
def test_creator_is_an_admin(shared_zone_test_context):
"""
@@ -114,7 +175,52 @@ def test_create_group_without_name_or_email(shared_zone_test_context):
"Missing Group.name",
"Missing Group.email"
))
+def test_create_group_with_invalid_email_domain(shared_zone_test_context):
+ """
+ Tests that creating a group With Invalid email fails
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ new_group = {
+ "name": "invalid-email",
+ "email": "test@abc.com",
+ "description": "this is a description",
+ "members": [{"id": "ok"}],
+ "admins": [{"id": "ok"}]
+ }
+ error = client.create_group(new_group, status=400)
+ assert_that(error, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
+def test_create_group_with_invalid_email(shared_zone_test_context):
+ """
+ Tests that creating a group With Invalid email fails
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ new_group = {
+ "name": "invalid-email",
+ "email": "test.abc.com",
+ "description": "this is a description",
+ "members": [{"id": "ok"}],
+ "admins": [{"id": "ok"}]
+ }
+ error = client.create_group(new_group, status=400)
+ assert_that(error, is_("Please enter a valid Email."))
+
+def test_create_group_with_invalid_email_number_of_dots(shared_zone_test_context):
+ """
+ Tests that creating a group With Invalid email fails
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ new_group = {
+ "name": "invalid-email",
+ "email": "test@ok.ok.dummy.com",
+ "description": "this is a description",
+ "members": [{"id": "ok"}],
+ "admins": [{"id": "ok"}]
+ }
+ error = client.create_group(new_group, status=400)
+ assert_that(error, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
def test_create_group_without_members_or_admins(shared_zone_test_context):
"""
diff --git a/modules/api/src/test/functional/tests/membership/get_group_changes_test.py b/modules/api/src/test/functional/tests/membership/get_group_changes_test.py
index f1221a57a..ea4880ee8 100644
--- a/modules/api/src/test/functional/tests/membership/get_group_changes_test.py
+++ b/modules/api/src/test/functional/tests/membership/get_group_changes_test.py
@@ -26,12 +26,6 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
# we grab 3 items, which when sorted by most recent will give the 3 most recent items
page_one = client.get_group_changes(created_group["id"], max_items=3, status=200)
- # our start from will align with the created on the 3rd change in the list
- start_from_index = 2
- # using epoch time to start from a timestamp
- epoch = datetime.utcfromtimestamp(0)
- # start from a known good timestamp
- start_from = str(int((datetime.strptime(page_one["changes"][start_from_index]["created"], "%Y-%m-%dT%H:%M:%S.%fZ") - epoch).total_seconds() * 1000))
# now, we say give me all changes since the start_from, which should yield 8-7-6-5-4
result = client.get_group_changes(created_group["id"], start_from=page_one["nextId"], max_items=5, status=200)
@@ -50,26 +44,19 @@ def test_list_group_activity_start_from_success(group_activity_context, shared_z
assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[expected_start - i - 1]))
-def test_list_group_activity_start_from_fake_time(group_activity_context, shared_zone_test_context):
+def test_list_group_activity_start_from_random_number(group_activity_context, shared_zone_test_context):
"""
- Test that we can start from a fake time stamp
+ Test that we can start from a random number, but it returns no changes
"""
client = shared_zone_test_context.ok_vinyldns_client
created_group = group_activity_context["created_group"]
updated_groups = group_activity_context["updated_groups"]
- start_from = "9999999999999" # start from a random timestamp far in the future
- result = client.get_group_changes(created_group["id"], start_from=start_from, max_items=5, status=200)
+ result = client.get_group_changes(created_group["id"], start_from=999, max_items=5, status=200)
# there are 10 updates, proceeded by 1 create
- assert_that(result["changes"], has_length(5))
+ assert_that(result["changes"], has_length(0))
assert_that(result["maxItems"], is_(5))
- assert_that(result["startFrom"], is_(start_from))
- assert_that(result["nextId"], is_not(none()))
-
- for i in range(0, 5):
- assert_that(result["changes"][i]["newGroup"], is_(updated_groups[9 - i]))
- assert_that(result["changes"][i]["oldGroup"], is_(updated_groups[9 - i - 1]))
def test_list_group_activity_max_item_success(group_activity_context, shared_zone_test_context):
@@ -151,9 +138,9 @@ def test_get_group_changes_paging(group_activity_context, shared_zone_test_conte
assert_that(page_three["changes"][0]["newGroup"], is_(created_group))
-def test_get_group_changes_unauthed(shared_zone_test_context):
+def test_get_group_changes_unauthorized(shared_zone_test_context):
"""
- Tests that non-group members can still get group changes
+ Tests that non-group members cannot get group changes
"""
client = shared_zone_test_context.ok_vinyldns_client
dummy_client = shared_zone_test_context.dummy_vinyldns_client
@@ -167,7 +154,7 @@ def test_get_group_changes_unauthed(shared_zone_test_context):
}
saved_group = client.create_group(new_group, status=200)
- dummy_client.get_group_changes(saved_group["id"], status=200)
+ dummy_client.get_group_changes(saved_group["id"], status=403)
client.get_group_changes(saved_group["id"], status=200)
finally:
if saved_group:
diff --git a/modules/api/src/test/functional/tests/recordsets/create_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/create_recordset_test.py
index bf6975041..742006aa6 100644
--- a/modules/api/src/test/functional/tests/recordsets/create_recordset_test.py
+++ b/modules/api/src/test/functional/tests/recordsets/create_recordset_test.py
@@ -681,6 +681,24 @@ def test_create_dotted_cname_record_succeeds_if_all_dotted_hosts_config_satisfie
client.wait_until_recordset_change_status(delete_result, "Complete")
+def test_create_IPv4_cname_record_fails(shared_zone_test_context):
+ """
+ Test that creating a CNAME record set as IPv4 address records returns an error.
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.parent_zone
+ apex_cname_rs = {
+ "zoneId": zone["id"],
+ "name": "test_create_cname_with_ipaddress",
+ "type": "CNAME",
+ "ttl": 500,
+ "records": [{"cname": "1.2.3.4"}]
+ }
+
+ error = client.create_recordset(apex_cname_rs, status=400)
+ assert_that(error, is_(f'Invalid CNAME: 1.2.3.4, valid CNAME record data cannot be an IP address.'))
+
+
def test_create_cname_with_multiple_records(shared_zone_test_context):
"""
Test that creating a CNAME record set with multiple records returns an error
@@ -1770,7 +1788,7 @@ def test_create_ipv4_ptr_recordset_with_verify_in_classless(shared_zone_test_con
try:
new_rs = {
"zoneId": reverse4_zone["id"],
- "name": "196",
+ "name": "193",
"type": "PTR",
"ttl": 100,
"records": [
diff --git a/modules/api/src/test/functional/tests/recordsets/get_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/get_recordset_test.py
index 7f0dc6621..5cd39864e 100644
--- a/modules/api/src/test/functional/tests/recordsets/get_recordset_test.py
+++ b/modules/api/src/test/functional/tests/recordsets/get_recordset_test.py
@@ -88,7 +88,60 @@ def test_get_recordset_doesnt_exist(shared_zone_test_context):
client.get_recordset(shared_zone_test_context.ok_zone["id"], "123", status=404)
-@pytest.mark.serial
+def test_get_recordset_count_status_code(shared_zone_test_context):
+ """
+ Test getting recordset count for a valid zoneid should return 200
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ client.get_recordset_count(shared_zone_test_context.ok_zone["id"],status=200)
+
+
+def test_get_recordset_count_error(shared_zone_test_context):
+ """
+ Test getting recordset count for a invalid zoneid should return 404
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ client.get_recordset_count("999",status=404)
+
+@pytest.mark.skip(reason="test is passing and failing with different result_recordset_count values")
+def test_get_recordset_count_by_zoneid(shared_zone_test_context):
+ """
+ Test getting a recordset with name @
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ ok_zone = shared_zone_test_context.ok_zone
+ result_rs = None
+ try:
+ new_rs = {
+ "zoneId": ok_zone["id"],
+ "name": "@",
+ "type": "TXT",
+ "ttl": 100,
+ "records": [
+ {
+ "text": "someText"
+ }
+ ]
+ }
+ result = client.create_recordset(new_rs, status=202)
+ result_rs = client.wait_until_recordset_change_status(result, "Complete")["recordSet"]
+
+ # Get the recordset we just made and verify
+ result = client.get_recordset(result_rs["zoneId"], result_rs["id"])
+ result_recordset_count = client.get_recordset_count(result_rs["zoneId"],status=200)
+ result_rs = result["recordSet"]
+
+ expected_rs = new_rs
+ expected_rs["name"] = ok_zone["name"]
+ verify_recordset(result_rs, expected_rs)
+
+ assert_that(result_recordset_count, is_({'count': 10}))
+ finally:
+ if result_rs:
+ delete_result = client.delete_recordset(result_rs["zoneId"], result_rs["id"], status=202)
+ client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
def test_at_get_recordset(shared_zone_test_context):
"""
Test getting a recordset with name @
diff --git a/modules/api/src/test/functional/tests/recordsets/list_recordset_changes_test.py b/modules/api/src/test/functional/tests/recordsets/list_recordset_changes_test.py
index 5c9f9a1c5..346256966 100644
--- a/modules/api/src/test/functional/tests/recordsets/list_recordset_changes_test.py
+++ b/modules/api/src/test/functional/tests/recordsets/list_recordset_changes_test.py
@@ -30,6 +30,39 @@ def check_changes_response(response, recordChanges=False, nextId=False, startFro
assert_that(change["userName"], is_("history-user"))
+def check_change_history_response(response, fqdn, type, recordChanges=False, nextId=False, startFrom=False,
+ maxItems=100):
+ """
+ :param type: type of the record
+ :param fqdn: fqdn of the record
+ :param response: return value of list_recordset_changes()
+ :param recordChanges: true if not empty or False if empty, cannot check exact values because don't have access to all attributes
+ :param nextId: true if exists, false if doesn't, wouldn't be able to check exact value
+ :param startFrom: the string for startFrom or false if doesnt exist
+ :param maxItems: maxItems is defined as an Int by default so will always return an Int
+ """
+ assert_that(response, has_key("zoneId")) # always defined as random string
+ if recordChanges:
+ assert_that(response["recordSetChanges"], is_not(has_length(0)))
+ else:
+ assert_that(response["recordSetChanges"], has_length(0))
+ if nextId:
+ assert_that(response, has_key("nextId"))
+ else:
+ assert_that(response, is_not(has_key("nextId")))
+ if startFrom:
+ assert_that(response["startFrom"], is_(startFrom))
+ else:
+ assert_that(response, is_not(has_key("startFrom")))
+ assert_that(response["maxItems"], is_(maxItems))
+
+ for change in response["recordSetChanges"]:
+ assert_that(change["userName"], is_("history-user"))
+ for recordset in change["recordSet"]:
+ assert_that(change["recordSet"]["type"], is_(type))
+ assert_that(change["recordSet"]["name"] + "." + response["recordSetChanges"][0]["zone"]["name"], is_(fqdn))
+
+
def test_list_recordset_changes_no_authorization(shared_zone_test_context):
"""
Test that recordset changes without authorization fails
@@ -143,12 +176,12 @@ def test_list_recordset_changes_exhausted(shared_zone_test_context):
def test_list_recordset_returning_no_changes(shared_zone_test_context):
"""
- Pass in startFrom of 0 should return empty list because start key is created time
+ Pass in startFrom of "2000" should return empty list because start key exceeded number of recordset changes
"""
client = shared_zone_test_context.history_client
original_zone = shared_zone_test_context.history_zone
- response = client.list_recordset_changes(original_zone["id"], start_from="0", max_items=None)
- check_changes_response(response, recordChanges=False, startFrom="0", nextId=False)
+ response = client.list_recordset_changes(original_zone["id"], start_from=2000, max_items=None)
+ check_changes_response(response, recordChanges=False, startFrom=2000, nextId=False)
def test_list_recordset_changes_default_max_items(shared_zone_test_context):
@@ -174,3 +207,109 @@ def test_list_recordset_changes_max_items_boundaries(shared_zone_test_context):
assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
+
+
+def test_list_recordset_history_no_authorization(shared_zone_test_context):
+ """
+ Test that recordset history without authorization fails
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+ client.list_recordset_change_history(zone_id, fqdn, type, sign_request=False, status=401)
+
+
+def test_list_recordset_history_member_auth_success(shared_zone_test_context):
+ """
+ Test recordset history succeeds with membership auth for member of admin group
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+ response = client.list_recordset_change_history(zone_id, fqdn, type, status=200)
+ check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
+
+
+def test_list_recordset_history_member_auth_no_access(shared_zone_test_context):
+ """
+ Test recordset history fails for user not in admin group with no acl rules
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+ client.list_recordset_change_history(zone_id, fqdn, type, status=403)
+
+
+def test_list_recordset_history_success(shared_zone_test_context):
+ """
+ Test recordset history succeeds with membership auth for member of admin group
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+ response = client.list_recordset_change_history(zone_id, fqdn, type, status=200)
+ check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False)
+
+
+def test_list_recordset_history_paging(shared_zone_test_context):
+ """
+ Test paging for recordset history can use previous nextId as start key of next page
+ """
+ client = shared_zone_test_context.history_client
+
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+
+ response_1 = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=1)
+ response_2 = client.list_recordset_change_history(zone_id, fqdn, type, start_from=response_1["nextId"], max_items=1)
+
+ check_change_history_response(response_1, fqdn, type, recordChanges=True, nextId=True, startFrom=False, maxItems=1)
+ check_change_history_response(response_2, fqdn, type, recordChanges=True, nextId=True, startFrom=response_1["nextId"], maxItems=1)
+
+
+def test_list_recordset_history_returning_no_changes(shared_zone_test_context):
+ """
+ Pass in startFrom of "2000" should return empty list because start key exceeded number of recordset change history
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+ response = client.list_recordset_change_history(zone_id, fqdn, type, start_from=2000, max_items=None)
+ assert_that(response["recordSetChanges"], has_length(0))
+ assert_that(response["startFrom"], is_(2000))
+ assert_that(response["maxItems"], is_(100))
+
+
+def test_list_recordset_history_default_max_items(shared_zone_test_context):
+ """
+ Test default max items is 100
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+
+ response = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=None)
+ check_change_history_response(response, fqdn, type, recordChanges=True, startFrom=False, nextId=False, maxItems=100)
+
+
+def test_list_recordset_history_max_items_boundaries(shared_zone_test_context):
+ """
+ Test 0 < max_items <= 100
+ """
+ client = shared_zone_test_context.history_client
+ zone_id = shared_zone_test_context.history_zone["id"]
+ fqdn = "test-create-cname-ok.system-test-history1."
+ type = "CNAME"
+
+ too_large = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=101, status=400)
+ too_small = client.list_recordset_change_history(zone_id, fqdn, type, start_from=None, max_items=0, status=400)
+
+ assert_that(too_large, is_("maxItems was 101, maxItems must be between 0 exclusive and 100 inclusive"))
+ assert_that(too_small, is_("maxItems was 0, maxItems must be between 0 exclusive and 100 inclusive"))
diff --git a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py
index cefbea85a..b7f6930f6 100644
--- a/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py
+++ b/modules/api/src/test/functional/tests/recordsets/update_recordset_test.py
@@ -303,7 +303,7 @@ def test_update_recordset_replace_2_records_with_1_different_record(shared_zone_
]
}
result = client.create_recordset(new_rs, status=202)
-
+
assert_that(result["changeType"], is_("Create"))
assert_that(result["status"], is_("Pending"))
assert_that(result["created"], is_not(none()))
@@ -373,7 +373,7 @@ def test_update_existing_record_set_add_record(shared_zone_test_context):
]
}
result = client.create_recordset(new_rs, status=202)
-
+
assert_that(result["changeType"], is_("Create"))
assert_that(result["status"], is_("Pending"))
assert_that(result["created"], is_not(none()))
@@ -1593,8 +1593,7 @@ def test_update_fails_for_unapplied_unsynced_record_change(shared_zone_test_cont
]
update_response = client.update_recordset(update_rs, status=202)
response = client.wait_until_recordset_change_status(update_response, "Failed")
- assert_that(response["systemMessage"], is_(f"Failed validating update to DNS for change \"{response['id']}\": \"{a_rs['name']}\": "
- f"This record set is out of sync with the DNS backend; sync this zone before attempting to update this record set."))
+ assert_that(response["systemMessage"], is_(f"This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."))
finally:
try:
delete_result = client.delete_recordset(zone["id"], create_rs["id"], status=202)
@@ -1789,6 +1788,115 @@ def test_update_from_unassociated_user_in_shared_zone_fails(shared_zone_test_con
delete_result = shared_client.delete_recordset(zone["id"], create_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+def test_update_from_super_user_in_shared_zone_passes_when_owner_group_is_only_update(shared_zone_test_context):
+ """
+ Test that updating with a superuser passes when the zone is set to shared and the owner group is the only change
+ """
+ super_user_client = shared_zone_test_context.super_user_client
+ shared_record_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ shared_zone = shared_zone_test_context.shared_zone
+ update_rs = None
+
+ try:
+ record_json = create_recordset(shared_zone, "test_shared_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_record_group["id"]
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_record_group["id"]))
+
+ update["ownerGroupId"] = dummy_group["id"]
+ update_response = super_user_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(shared_zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+def test_update_from_unassociated_user_in_shared_zone_fails_when_owner_group_is_only_update(shared_zone_test_context):
+ """
+ Test that updating with a user not in the record owner group fails when the zone is set to shared and
+ the owner group is the only change
+ """
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ shared_record_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ shared_zone = shared_zone_test_context.shared_zone
+ create_rs = None
+
+ try:
+ record_json = create_recordset(shared_zone, "test_shared_fail", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_record_group["id"]
+ create_response = shared_client.create_recordset(record_json, status=202)
+ create_rs = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(create_rs["ownerGroupId"], is_(shared_record_group["id"]))
+
+ update = create_rs
+ update["ownerGroupId"] = dummy_group["id"]
+ error = ok_client.update_recordset(update, status=422)
+ assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
+ finally:
+ if create_rs:
+ delete_result = shared_client.delete_recordset(shared_zone["id"], create_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+def test_update_from_super_user_in_private_zone_fails_when_owner_group_is_only_update(shared_zone_test_context):
+ """
+ Test that updating with a superuser fails when the zone is set to private and the owner group is the only change
+ """
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ super_user_client = shared_zone_test_context.super_user_client
+ ok_record_group = shared_zone_test_context.ok_group
+ dummy_group = shared_zone_test_context.dummy_group
+ ok_zone = shared_zone_test_context.ok_zone
+ create_rs = None
+
+ try:
+ record_json = create_recordset(ok_zone, "test_private_fail", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = ok_record_group["id"]
+ create_response = ok_client.create_recordset(record_json, status=202)
+ create_rs = ok_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(create_rs["ownerGroupId"], is_(ok_record_group["id"]))
+
+ update = create_rs
+ update["ownerGroupId"] = dummy_group["id"]
+ error = super_user_client.update_recordset(update, status=403)
+ assert_that(error, is_(f'User super-user does not have access to update test-private-fail.{ok_zone["name"]}'))
+ finally:
+ if create_rs:
+ delete_result = ok_client.delete_recordset(ok_zone["id"], create_rs["id"], status=202)
+ ok_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+def test_update_from_super_user_in_shared_zone_fails_when_owner_group_is_not_the_only_update(shared_zone_test_context):
+ """
+ Test that updating with a superuser fails when the zone is set to shared and the owner group is not the only change
+ """
+ super_user_client = shared_zone_test_context.super_user_client
+ shared_record_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ shared_zone = shared_zone_test_context.shared_zone
+ create_rs = None
+
+ try:
+ record_json = create_recordset(shared_zone, "test_shared_fail", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_record_group["id"]
+ create_response = shared_client.create_recordset(record_json, status=202)
+ create_rs = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(create_rs["ownerGroupId"], is_(shared_record_group["id"]))
+
+ update = create_rs
+ update["ownerGroupId"] = dummy_group["id"]
+ update["ttl"] = update["ttl"] + 100
+ error = super_user_client.update_recordset(update, status=403)
+ assert_that(error, is_(f'User super-user does not have access to update test-shared-fail.{shared_zone["name"]}'))
+ finally:
+ if create_rs:
+ delete_result = shared_client.delete_recordset(shared_zone["id"], create_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
@pytest.mark.serial
def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
@@ -1819,6 +1927,572 @@ def test_update_from_acl_for_shared_zone_passes(shared_zone_test_context):
delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+def test_update_owner_group_transfer_auto_approved(shared_zone_test_context):
+ """
+ Test auto approve ownerShip transfer, for shared zones
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved",
+ "requestedOwnerGroupId": ok_group["id"]}
+
+ update["recordSetGroupChange"] = recordset_group_change_json
+ update_response = shared_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_json))
+ assert_that(update_rs["ownerGroupId"], is_(ok_group["id"]))
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_request(shared_zone_test_context):
+ """
+ Test requesting ownerShip transfer, for shared zones
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = dummy_group["id"]
+
+ create_response = dummy_client.create_recordset(record_json, status=202)
+ update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": shared_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": shared_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = shared_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_request_owner_group_transfer_manually_approved(shared_zone_test_context):
+ """
+ Test approving ownerShip transfer request, for shared zones
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
+ recordset_group_change_manually_approved_json = {"ownerShipTransferStatus": "ManuallyApproved",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = shared_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
+ "recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_approved_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(ok_group["id"]))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_request_owner_group_transfer_manually_rejected(shared_zone_test_context):
+ """
+ Test rejecting ownerShip transfer request, for shared zones
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
+ recordset_group_change_manually_rejected_json = {"ownerShipTransferStatus": "ManuallyRejected",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = shared_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = shared_client.wait_until_recordset_change_status(update_rs_response, "Complete")[
+ "recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_manually_rejected_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_request_owner_group_transfer_cancelled(shared_zone_test_context):
+ """
+ Test cancelling ownerShip transfer request
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
+ recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = ok_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_approval_to_group_a_user_is_not_in_fails(shared_zone_test_context):
+ """
+ Test approving ownerShip transfer request, for user not a member of owner group
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = dummy_group["id"]
+
+ create_response = dummy_client.create_recordset(record_json, status=202)
+ update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": shared_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": shared_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = shared_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyApproved"}
+
+ update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
+ error = shared_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_reject_to_group_a_user_is_not_in_fails(shared_zone_test_context):
+ """
+ Test rejecting ownerShip transfer request, for user not a member of owner group
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = dummy_group["id"]
+
+ create_response = dummy_client.create_recordset(record_json, status=202)
+ update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": shared_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": shared_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = shared_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_approved_json = {"ownerShipTransferStatus": "ManuallyRejected"}
+ update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
+ error = shared_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_(f"User not in record owner group with id \"{dummy_group['id']}\""))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_auto_approved_to_group_a_user_is_not_in_fails(shared_zone_test_context):
+ """
+ Test approving ownerShip transfer request, for user not a member of owner group
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = dummy_group["id"]
+
+ create_response = dummy_client.create_recordset(record_json, status=202)
+ update = dummy_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": shared_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": shared_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = shared_client.update_recordset(update, status=202)
+ update_rs = shared_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(dummy_group["id"]))
+
+ recordset_group_change_approved_json = {"ownerShipTransferStatus": "AutoApproved"}
+
+ update_rs["recordSetGroupChange"] = recordset_group_change_approved_json
+ error = shared_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_(f"Record owner group with id \"{dummy_group['id']}\" not found"))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_approved_when_request_cancelled_in_fails(shared_zone_test_context):
+ """
+ Test approving ownerShip transfer, for cancelled request
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
+ recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = ok_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyApproved"}
+
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ error = ok_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_rejected_when_request_cancelled_in_fails(shared_zone_test_context):
+ """
+ Test rejecting ownerShip transfer, for cancelled request
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
+ recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = ok_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "ManuallyRejected"}
+
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ error = ok_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_auto_approved_when_request_cancelled_in_fails(shared_zone_test_context):
+ """
+ Test auto_approving ownerShip transfer, for cancelled request
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_group = shared_zone_test_context.ok_group
+
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": ok_group["id"]}
+ recordset_group_change_pending_review_json = {"ownerShipTransferStatus": "PendingReview",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ update_response = ok_client.update_recordset(update, status=202)
+ update_rs = ok_client.wait_until_recordset_change_status(update_response, "Complete")["recordSet"]
+ assert_that(update_rs["recordSetGroupChange"], is_(recordset_group_change_pending_review_json))
+ assert_that(update_rs["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Cancelled"}
+ recordset_group_change_cancelled_json = {"ownerShipTransferStatus": "Cancelled",
+ "requestedOwnerGroupId": ok_group["id"]}
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ update_rs_response = ok_client.update_recordset(update_rs, status=202)
+ update_rs_ownership = ok_client.wait_until_recordset_change_status(update_rs_response, "Complete")["recordSet"]
+ assert_that(update_rs_ownership["recordSetGroupChange"], is_(recordset_group_change_cancelled_json))
+ assert_that(update_rs_ownership["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "AutoApproved"}
+
+ update_rs["recordSetGroupChange"] = recordset_group_change_json
+ error = ok_client.update_recordset(update_rs, status=422)
+ assert_that(error, is_("Cannot update RecordSet OwnerShip Status when request is cancelled."))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_on_non_shared_zones_in_fails(shared_zone_test_context):
+ """
+ Test that requesting ownerShip transfer for non shared zones
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ ok_client = shared_zone_test_context.ok_vinyldns_client
+ shared_group = shared_zone_test_context.shared_record_group
+ ok_zone = shared_zone_test_context.ok_zone
+
+ update_rs = None
+
+ try:
+ # record_json = create_recordset(ok_zone, "test_update_success", "A", [{"address": "1.1.1.1"}], recordSetGroupChange={"ownerShipTransferStatus": None, "requestedOwnerGroupId": None})
+ record_json = {
+ "zoneId": ok_zone["id"],
+ "name": "test_update_success",
+ "type": "A",
+ "ttl": 38400,
+ "records": [
+ {"address": "1.1.1.1"}
+ ],
+ "recordSetGroupChange": {"ownerShipTransferStatus": "None",
+ "requestedOwnerGroupId": None}
+ }
+
+ create_response = ok_client.create_recordset(record_json, status=202)
+ update = ok_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": shared_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+
+ error = shared_client.update_recordset(update, status=422)
+ assert_that(error, is_("Cannot update RecordSet OwnerShip Status when zone is not shared."))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
+
+def test_update_owner_group_transfer_and_ttl_on_user_not_in_owner_group_in_fails(shared_zone_test_context):
+ """
+ Test that updating record "i.e.ttl" with requesting ownerShip transfer, where user not in the member of the owner group
+ """
+ shared_client = shared_zone_test_context.shared_zone_vinyldns_client
+ dummy_client = shared_zone_test_context.dummy_vinyldns_client
+ zone = shared_zone_test_context.shared_zone
+ shared_group = shared_zone_test_context.shared_record_group
+ dummy_group = shared_zone_test_context.dummy_group
+ update_rs = None
+
+ try:
+ record_json = create_recordset(zone, "test_shared_admin_update_success", "A", [{"address": "1.1.1.1"}])
+ record_json["ownerGroupId"] = shared_group["id"]
+
+ create_response = shared_client.create_recordset(record_json, status=202)
+ update = shared_client.wait_until_recordset_change_status(create_response, "Complete")["recordSet"]
+ assert_that(update["ownerGroupId"], is_(shared_group["id"]))
+
+ recordset_group_change_json = {"ownerShipTransferStatus": "Requested",
+ "requestedOwnerGroupId": dummy_group["id"]}
+ update["recordSetGroupChange"] = recordset_group_change_json
+ update["ttl"] = update["ttl"] + 100
+
+ error = dummy_client.update_recordset(update, status=422)
+ assert_that(error, is_(f"Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"))
+
+ finally:
+ if update_rs:
+ delete_result = shared_client.delete_recordset(zone["id"], update_rs["id"], status=202)
+ shared_client.wait_until_recordset_change_status(delete_result, "Complete")
+
def test_update_to_no_group_owner_passes(shared_zone_test_context):
"""
diff --git a/modules/api/src/test/functional/tests/shared_zone_test_context.py b/modules/api/src/test/functional/tests/shared_zone_test_context.py
index 3bef09088..a27c52018 100644
--- a/modules/api/src/test/functional/tests/shared_zone_test_context.py
+++ b/modules/api/src/test/functional/tests/shared_zone_test_context.py
@@ -28,12 +28,14 @@ class SharedZoneTestContext(object):
self.dummy_vinyldns_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "dummyAccessKey", "dummySecretKey")
self.shared_zone_vinyldns_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "sharedZoneUserAccessKey", "sharedZoneUserSecretKey")
self.support_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "supportUserAccessKey", "supportUserSecretKey")
+ self.super_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "superUserAccessKey", "superUserSecretKey")
self.unassociated_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "listGroupAccessKey", "listGroupSecretKey")
self.test_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "testUserAccessKey", "testUserSecretKey")
self.history_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "history-key", "history-secret")
self.non_user_client = VinylDNSClient(VinylDNSTestContext.vinyldns_url, "not-exist-key", "not-exist-secret")
- self.clients = [self.ok_vinyldns_client, self.dummy_vinyldns_client, self.shared_zone_vinyldns_client, self.support_user_client,
- self.unassociated_client, self.test_user_client, self.history_client, self.non_user_client]
+ self.clients = [self.ok_vinyldns_client, self.dummy_vinyldns_client, self.shared_zone_vinyldns_client,
+ self.support_user_client, self.super_user_client, self.unassociated_client,
+ self.test_user_client, self.history_client, self.non_user_client]
self.list_zones = ListZonesTestContext(partition_id)
self.list_zones_client = self.list_zones.client
self.list_records_context = ListRecordSetsTestContext(partition_id)
diff --git a/modules/api/src/test/functional/tests/zones/create_zone_test.py b/modules/api/src/test/functional/tests/zones/create_zone_test.py
index fbb5bed16..40d34ee88 100644
--- a/modules/api/src/test/functional/tests/zones/create_zone_test.py
+++ b/modules/api/src/test/functional/tests/zones/create_zone_test.py
@@ -95,6 +95,49 @@ def test_create_zone_success(shared_zone_test_context):
client.abandon_zones([result_zone["id"]], status=202)
+def test_create_zone_success_number_of_dots(shared_zone_test_context):
+ """
+ Test successfully creating a zone
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ # Include a space in the zone name to verify that it is trimmed and properly formatted
+ zone_name = f"one-time{shared_zone_test_context.partition_id} "
+
+ zone = {
+ "name": zone_name,
+ "email": "test@ok.dummy.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "backendId": "func-test-backend"
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ get_result = client.get_zone(result_zone["id"])
+
+ get_zone = get_result["zone"]
+ assert_that(get_zone["name"], is_(zone["name"].strip() + "."))
+ assert_that(get_zone["email"], is_(zone["email"]))
+ assert_that(get_zone["adminGroupId"], is_(zone["adminGroupId"]))
+ assert_that(get_zone["latestSync"], is_not(none()))
+ assert_that(get_zone["status"], is_("Active"))
+ assert_that(get_zone["backendId"], is_("func-test-backend"))
+
+ # confirm that the recordsets in DNS have been saved in vinyldns
+ recordsets = client.list_recordsets_by_zone(result_zone["id"])["recordSets"]
+
+ assert_that(len(recordsets), is_(7))
+ for rs in recordsets:
+ small_rs = dict((k, rs[k]) for k in ["name", "type", "records"])
+ small_rs["records"] = small_rs["records"]
+ assert_that(retrieve_dns_records(shared_zone_test_context), has_item(small_rs))
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+
@pytest.mark.skip_production
def test_create_zone_without_transfer_connection_leaves_it_empty(shared_zone_test_context):
"""
@@ -179,6 +222,59 @@ def test_create_invalid_zone_data(shared_zone_test_context):
errors = client.create_zone(zone, status=400)["errors"]
assert_that(errors, contains_inanyorder("Do not know how to convert JString(invalid_value) into boolean"))
+def test_create_invalid_email(shared_zone_test_context):
+ """
+ Test that creating a zone with invalid email
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ zone_name = f"one-time{shared_zone_test_context.partition_id} "
+
+ zone = {
+ "name": zone_name,
+ "email": "test.abc.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "backendId": "func-test-backend"
+ }
+
+ errors = client.create_zone(zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email."))
+
+def test_create_invalid_email_number_of_dots(shared_zone_test_context):
+ """
+ Test that creating a zone with invalid email
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ zone_name = f"one-time{shared_zone_test_context.partition_id} "
+
+ zone = {
+ "name": zone_name,
+ "email": "test@abc.ok.dummy.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "backendId": "func-test-backend"
+ }
+
+ errors = client.create_zone(zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
+
+def test_create_invalid_domain(shared_zone_test_context):
+ """
+ Test that creating a zone with invalid domain
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ zone_name = f"one-time{shared_zone_test_context.partition_id} "
+
+ zone = {
+ "name": zone_name,
+ "email": "test@abc.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "backendId": "func-test-backend"
+ }
+
+ errors = client.create_zone(zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
@pytest.mark.serial
def test_create_zone_with_connection_failure(shared_zone_test_context):
diff --git a/modules/api/src/test/functional/tests/zones/get_zone_test.py b/modules/api/src/test/functional/tests/zones/get_zone_test.py
index 0068c2bfa..1ff676855 100644
--- a/modules/api/src/test/functional/tests/zones/get_zone_test.py
+++ b/modules/api/src/test/functional/tests/zones/get_zone_test.py
@@ -46,6 +46,7 @@ def test_get_zone_shared_by_id_non_owner(shared_zone_test_context):
assert_that(retrieved["shared"], is_(True))
assert_that(retrieved["accessLevel"], is_("Read"))
+
def test_get_zone_private_by_id_fails_without_access(shared_zone_test_context):
"""
Test get an existing zone by id without access
@@ -72,6 +73,29 @@ def test_get_zone_by_id_no_authorization(shared_zone_test_context):
client.get_zone("123456", sign_request=False, status=401)
+def test_get_common_zone_details_by_id(shared_zone_test_context):
+ """
+ Test get an existing zone's common details by id
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+
+ result = client.get_common_zone_details(shared_zone_test_context.system_test_zone["id"], status=200)
+ retrieved = result["zone"]
+
+ assert_that(retrieved["name"], is_(shared_zone_test_context.system_test_zone["name"]))
+ assert_that(retrieved["email"], is_(shared_zone_test_context.system_test_zone["email"]))
+ assert_that(retrieved["adminGroupName"], is_(shared_zone_test_context.ok_group["name"]))
+ assert_that(retrieved["adminGroupId"], is_(shared_zone_test_context.ok_group["id"]))
+
+
+def test_get_common_zone_details_by_id_returns_404_when_not_found(shared_zone_test_context):
+ """
+ Test get an existing zone returns a 404 when the zone is not found
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ client.get_common_zone_details(str(uuid.uuid4()), status=404)
+
+
@pytest.mark.serial
def test_get_zone_by_id_includes_acl_display_name(shared_zone_test_context):
"""
diff --git a/modules/api/src/test/functional/tests/zones/list_zone_changes_test.py b/modules/api/src/test/functional/tests/zones/list_zone_changes_test.py
index ff0345f22..a6d2afacb 100644
--- a/modules/api/src/test/functional/tests/zones/list_zone_changes_test.py
+++ b/modules/api/src/test/functional/tests/zones/list_zone_changes_test.py
@@ -53,7 +53,7 @@ def test_list_zone_changes_member_auth_no_access(shared_zone_test_context):
@pytest.mark.serial
def test_list_zone_changes_member_auth_with_acl(shared_zone_test_context):
"""
- Test list zone changes succeeds for user with acl rules
+ Test list zone changes fails for user with acl rules
"""
zone = shared_zone_test_context.ok_zone
acl_rule = generate_acl_rule("Write", userId="dummy")
@@ -62,7 +62,7 @@ def test_list_zone_changes_member_auth_with_acl(shared_zone_test_context):
client.list_zone_changes(zone["id"], status=403)
add_ok_acl_rules(shared_zone_test_context, [acl_rule])
- client.list_zone_changes(zone["id"], status=200)
+ client.list_zone_changes(zone["id"], status=403)
finally:
clear_ok_acl_rules(shared_zone_test_context)
diff --git a/modules/api/src/test/functional/tests/zones/update_zone_test.py b/modules/api/src/test/functional/tests/zones/update_zone_test.py
index 5a964bd8c..f4588e91e 100644
--- a/modules/api/src/test/functional/tests/zones/update_zone_test.py
+++ b/modules/api/src/test/functional/tests/zones/update_zone_test.py
@@ -4,6 +4,7 @@ import pytest
from utils import *
from vinyldns_context import VinylDNSTestContext
+from datetime import datetime, timezone, timedelta
@pytest.mark.serial
@@ -45,7 +46,7 @@ def test_update_zone_success(shared_zone_test_context):
result_zone = result["zone"]
client.wait_until_zone_active(result_zone["id"])
- result_zone["email"] = "foo@bar.com"
+ result_zone["email"] = "test@dummy.com"
result_zone["acl"]["rules"] = [acl_rule]
update_result = client.update_zone(result_zone, status=202)
client.wait_until_zone_change_status_synced(update_result)
@@ -57,7 +58,7 @@ def test_update_zone_success(shared_zone_test_context):
get_result = client.get_zone(result_zone["id"])
uz = get_result["zone"]
- assert_that(uz["email"], is_("foo@bar.com"))
+ assert_that(uz["email"], is_("test@dummy.com"))
assert_that(uz["updated"], is_not(none()))
acl = uz["acl"]
@@ -66,6 +67,378 @@ def test_update_zone_success(shared_zone_test_context):
if result_zone:
client.abandon_zones([result_zone["id"]], status=202)
+def test_update_zone_success_wildcard(shared_zone_test_context):
+ """
+ Test updating a zone for email validation wildcard
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ acl_rule = {
+ "accessLevel": "Read",
+ "description": "test-acl-updated-by-updatezn",
+ "userId": "ok",
+ "recordMask": "www-*",
+ "recordTypes": ["A", "AAAA", "CNAME"]
+ }
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ result_zone["email"] = "test@ok.dummy.com"
+ result_zone["acl"]["rules"] = [acl_rule]
+ update_result = client.update_zone(result_zone, status=202)
+ client.wait_until_zone_change_status_synced(update_result)
+
+ assert_that(update_result["changeType"], is_("Update"))
+ assert_that(update_result["userId"], is_("ok"))
+ assert_that(update_result, has_key("created"))
+
+ get_result = client.get_zone(result_zone["id"])
+
+ uz = get_result["zone"]
+ assert_that(uz["email"], is_("test@ok.dummy.com"))
+ assert_that(uz["updated"], is_not(none()))
+
+ acl = uz["acl"]
+ verify_acl_rule_is_present_once(acl_rule, acl)
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+def test_update_zone_success_number_of_dots(shared_zone_test_context):
+ """
+ Test updating a zone for email validation wildcard
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ acl_rule = {
+ "accessLevel": "Read",
+ "description": "test-acl-updated-by-updatezn",
+ "userId": "ok",
+ "recordMask": "www-*",
+ "recordTypes": ["A", "AAAA", "CNAME"]
+ }
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ result_zone["email"] = "test@ok.dummy.com"
+ result_zone["acl"]["rules"] = [acl_rule]
+ update_result = client.update_zone(result_zone, status=202)
+ client.wait_until_zone_change_status_synced(update_result)
+
+ assert_that(update_result["changeType"], is_("Update"))
+ assert_that(update_result["userId"], is_("ok"))
+ assert_that(update_result, has_key("created"))
+
+ get_result = client.get_zone(result_zone["id"])
+
+ uz = get_result["zone"]
+ assert_that(uz["email"], is_("test@ok.dummy.com"))
+ assert_that(uz["updated"], is_not(none()))
+
+ acl = uz["acl"]
+ verify_acl_rule_is_present_once(acl_rule, acl)
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+def test_update_invalid_email(shared_zone_test_context):
+ """
+ Test that updating a zone with invalid email
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ acl_rule = {
+ "accessLevel": "Read",
+ "description": "test-acl-updated-by-updatezn",
+ "userId": "ok",
+ "recordMask": "www-*",
+ "recordTypes": ["A", "AAAA", "CNAME"]
+ }
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ result_zone["email"] = "test.trial.com"
+ errors = client.update_zone(result_zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email."))
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+def test_update_invalid_domain(shared_zone_test_context):
+ """
+ Test that updating a zone with invalid domain
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ acl_rule = {
+ "accessLevel": "Read",
+ "description": "test-acl-updated-by-updatezn",
+ "userId": "ok",
+ "recordMask": "www-*",
+ "recordTypes": ["A", "AAAA", "CNAME"]
+ }
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ result_zone["email"] = "test@trial.com"
+ errors = client.update_zone(result_zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email. Valid domains should end with test.com,dummy.com"))
+
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+def test_update_invalid_email_number_of_dots(shared_zone_test_context):
+ """
+ Test that updating a zone with invalid domain
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ acl_rule = {
+ "accessLevel": "Read",
+ "description": "test-acl-updated-by-updatezn",
+ "userId": "ok",
+ "recordMask": "www-*",
+ "recordTypes": ["A", "AAAA", "CNAME"]
+ }
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ result_zone["email"] = "test@ok.ok.dummy.com"
+ errors = client.update_zone(result_zone, status=400)
+ assert_that(errors, is_("Please enter a valid Email. Number of dots allowed after @ is 2"))
+
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+def test_update_zone_sync_schedule_fails(shared_zone_test_context):
+ """
+ Test updating a zone with a schedule for zone sync fails when the user is not an admin user
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ # schedule zone sync every 5 seconds
+ result_zone["recurrenceSchedule"] = "0/5 0 0 ? * * *"
+ error = client.update_zone(result_zone, status=403)
+
+ assert_that(error, contains_string("User 'ok' is not authorized to schedule zone sync in this zone."))
+
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
+
+def test_update_zone_sync_schedule_success(shared_zone_test_context):
+ """
+ Test updating a zone with a schedule for zone sync successfully sync zone at scheduled time
+ """
+ client = shared_zone_test_context.ok_vinyldns_client
+ result_zone = None
+ try:
+ zone_name = f"one-time{shared_zone_test_context.partition_id}"
+
+ zone = {
+ "name": zone_name,
+ "email": "test@test.com",
+ "adminGroupId": shared_zone_test_context.ok_group["id"],
+ "connection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ },
+ "transferConnection": {
+ "name": "vinyldns.",
+ "keyName": VinylDNSTestContext.dns_key_name,
+ "key": VinylDNSTestContext.dns_key,
+ "primaryServer": VinylDNSTestContext.name_server_ip
+ }
+ }
+ result = client.create_zone(zone, status=202)
+ result_zone = result["zone"]
+ client.wait_until_zone_active(result_zone["id"])
+
+ # schedule zone sync every 5 seconds
+ result_zone["recurrenceSchedule"] = "0/5 0 0 ? * * *"
+
+ # Get the current time in the local timezone
+ now = datetime.now()
+
+ # Convert the time to the UTC timezone
+ utc_time = now.astimezone(timezone.utc)
+
+ super_user_client = shared_zone_test_context.support_user_client
+
+ update_result = super_user_client.update_zone(result_zone, status=202)
+ super_user_client.wait_until_zone_change_status_synced(update_result)
+
+ assert_that(update_result["changeType"], is_("Update"))
+ assert_that(update_result["userId"], is_("support-user-id"))
+ assert_that(update_result, has_key("created"))
+
+ get_result = client.get_zone(result_zone["id"])
+
+ uz = get_result["zone"]
+ assert_that(uz["recurrenceSchedule"], is_("0/5 0 0 ? * * *"))
+ assert_that(uz["updated"], is_not(none()))
+
+ # Add + or - 2 seconds to the current time as there may be a slight change than the exact scheduled time
+ utc_time_1 = utc_time - timedelta(seconds=1)
+ utc_time_2 = utc_time - timedelta(seconds=2)
+ utc_time_3 = utc_time + timedelta(seconds=1)
+ utc_time_4 = utc_time + timedelta(seconds=2)
+
+ # Format the time as a string in the desired format
+ time_str = utc_time.strftime('%Y-%m-%dT%H:%M:%SZ')
+ time_str_1 = utc_time_1.strftime('%Y-%m-%dT%H:%M:%SZ')
+ time_str_2 = utc_time_2.strftime('%Y-%m-%dT%H:%M:%SZ')
+ time_str_3 = utc_time_3.strftime('%Y-%m-%dT%H:%M:%SZ')
+ time_str_4 = utc_time_4.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ time_list = [time_str, time_str_1, time_str_2, time_str_3, time_str_4]
+
+ # Check if zone sync was performed at scheduled time
+ assert_that(time_list, has_item(uz["latestSync"]))
+
+ finally:
+ if result_zone:
+ client.abandon_zones([result_zone["id"]], status=202)
+
def test_update_bad_acl_fails(shared_zone_test_context):
"""
@@ -318,12 +691,12 @@ def test_create_acl_user_rule_invalid_cidr_failure(shared_zone_test_context):
"accessLevel": "Read",
"description": "test-acl-user-id",
"userId": "789",
- "recordMask": "10.0.0.0/50",
+ "recordMask": "10.0.0/50",
"recordTypes": ["PTR"]
}
errors = client.add_zone_acl_rule(shared_zone_test_context.ip4_reverse_zone["id"], acl_rule, status=400)
- assert_that(errors, contains_string("PTR types must have no mask or a valid CIDR mask: IPv4 mask must be between 0 and 32"))
+ assert_that(errors, contains_string("PTR types must have no mask or a valid CIDR mask: Invalid CIDR block"))
@pytest.mark.serial
@@ -626,7 +999,7 @@ def test_update_reverse_v4_zone(shared_zone_test_context):
client = shared_zone_test_context.ok_vinyldns_client
zone = copy.deepcopy(shared_zone_test_context.ip4_reverse_zone)
- zone["email"] = "update-test@bar.com"
+ zone["email"] = "test@test.com"
update_result = client.update_zone(zone, status=202)
client.wait_until_zone_change_status_synced(update_result)
@@ -638,7 +1011,7 @@ def test_update_reverse_v4_zone(shared_zone_test_context):
get_result = client.get_zone(zone["id"])
uz = get_result["zone"]
- assert_that(uz["email"], is_("update-test@bar.com"))
+ assert_that(uz["email"], is_("test@test.com"))
assert_that(uz["updated"], is_not(none()))
@@ -649,7 +1022,7 @@ def test_update_reverse_v6_zone(shared_zone_test_context):
client = shared_zone_test_context.ok_vinyldns_client
zone = copy.deepcopy(shared_zone_test_context.ip6_reverse_zone)
- zone["email"] = "update-test@bar.com"
+ zone["email"] = "test@test.com"
update_result = client.update_zone(zone, status=202)
client.wait_until_zone_change_status_synced(update_result)
@@ -661,7 +1034,7 @@ def test_update_reverse_v6_zone(shared_zone_test_context):
get_result = client.get_zone(zone["id"])
uz = get_result["zone"]
- assert_that(uz["email"], is_("update-test@bar.com"))
+ assert_that(uz["email"], is_("test@test.com"))
assert_that(uz["updated"], is_not(none()))
@@ -831,6 +1204,7 @@ def test_normal_user_cannot_update_shared_zone_flag(shared_zone_test_context):
error = shared_zone_test_context.ok_vinyldns_client.update_zone(zone_update, status=403)
assert_that(error, contains_string("Not authorized to update zone shared status from false to true."))
+
@pytest.mark.serial
def test_update_connection_info_success(shared_zone_test_context):
"""
diff --git a/modules/api/src/test/functional/utils.py b/modules/api/src/test/functional/utils.py
index 449119e29..9ebac95d8 100644
--- a/modules/api/src/test/functional/utils.py
+++ b/modules/api/src/test/functional/utils.py
@@ -509,6 +509,57 @@ def get_change_MX_json(input_name, ttl=200, preference=None, exchange=None, chan
return json
+def get_change_NS_json(input_name, ttl=200, nsdname=None, change_type="Add"):
+ json = {
+ "changeType": change_type,
+ "inputName": input_name,
+ "type": "NS"
+ }
+ if change_type == "Add":
+ json["ttl"] = ttl
+ json["record"] = {
+ "nsdname": nsdname
+ }
+ return json
+
+
+def get_change_SRV_json(input_name, ttl=200, priority=None, weight=None, port=None, target=None, change_type="Add"):
+ json = {
+ "changeType": change_type,
+ "inputName": input_name,
+ "type": "SRV",
+ }
+ if change_type == "Add":
+ json["ttl"] = ttl
+ json["record"] = {
+ "priority": priority,
+ "weight": weight,
+ "port": port,
+ "target": target
+ }
+ return json
+
+
+def get_change_NAPTR_json(input_name, ttl=200, order=None, preference=None, flags=None, service=None, regexp=None, replacement=None, change_type="Add"):
+ json = {
+ "changeType": change_type,
+ "inputName": input_name,
+ "type": "NAPTR",
+
+ }
+ if change_type == "Add":
+ json["ttl"] = ttl
+ json["record"] = {
+ "order": order,
+ "preference": preference,
+ "flags": flags,
+ "service": service,
+ "regexp": regexp,
+ "replacement": replacement
+ }
+ return json
+
+
def create_recordset(zone, rname, recordset_type, rdata_list, ttl=200, ownergroup_id=None):
recordset_data = {
"zoneId": zone["id"],
diff --git a/modules/api/src/test/functional/vinyldns_python.py b/modules/api/src/test/functional/vinyldns_python.py
index 87c7f8866..066965e51 100644
--- a/modules/api/src/test/functional/vinyldns_python.py
+++ b/modules/api/src/test/functional/vinyldns_python.py
@@ -51,7 +51,8 @@ class VinylDNSClient(object):
self.session.close()
self.session_not_found_ok.close()
- def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504), session=None):
+ def requests_retry_not_found_ok_session(self, retries=20, backoff_factor=0.1, status_forcelist=(500, 502, 504),
+ session=None):
session = session or requests.Session()
retry = Retry(
total=retries,
@@ -79,7 +80,8 @@ class VinylDNSClient(object):
session.mount("https://", adapter)
return session
- def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False, **kwargs):
+ def make_request(self, url, method="GET", headers=None, body_string=None, sign_request=True, not_found_ok=False,
+ **kwargs):
# pull out status or None
status_code = kwargs.pop("status", None)
@@ -100,13 +102,15 @@ class VinylDNSClient(object):
for k, v in query.items())
if sign_request:
- signed_headers, signed_body = self.sign_request(method, path, body_string, query, with_headers=headers or {}, **kwargs)
+ signed_headers, signed_body = self.sign_request(method, path, body_string, query,
+ with_headers=headers or {}, **kwargs)
else:
signed_headers = headers or {}
signed_body = body_string
if not_found_ok:
- response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
+ response = self.session_not_found_ok.request(method, url, data=signed_body, headers=signed_headers,
+ **kwargs)
else:
response = self.session.request(method, url, data=signed_body, headers=signed_headers, **kwargs)
@@ -388,6 +392,17 @@ class VinylDNSClient(object):
return data
+ def get_common_zone_details(self, zone_id, **kwargs):
+ """
+ Gets common zone details which can be seen by all users for the given zone id
+ :param zone_id: the id of the zone to retrieve
+ :return: the zone, or will 404 if not found
+ """
+ url = urljoin(self.index_url, "/zones/{0}/details".format(zone_id))
+ response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
+
+ return data
+
def get_zone_by_name(self, zone_name, **kwargs):
"""
Gets a zone for the given zone name
@@ -445,7 +460,31 @@ class VinylDNSClient(object):
response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
return data
- def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False, ignore_access=False, **kwargs):
+ def list_recordset_change_history(self, zone_id, fqdn, record_type, start_from=None, max_items=None, **kwargs):
+ """
+ Gets the record's change history for the given zone, record fqdn and record type
+ :param zone_id: the id of the zone to retrieve
+ :param fqdn: the record's fqdn
+ :param record_type: the record's type
+ :param start_from: the start key of the page
+ :param max_items: the page limit
+ :return: the zone, or will 404 if not found
+ """
+ args = []
+ if start_from:
+ args.append("startFrom={0}".format(start_from))
+ if max_items is not None:
+ args.append("maxItems={0}".format(max_items))
+ args.append("zoneId={0}".format(zone_id))
+ args.append("fqdn={0}".format(fqdn))
+ args.append("recordType={0}".format(record_type))
+ url = urljoin(self.index_url, "recordsetchange/history") + "?" + "&".join(args)
+
+ response, data = self.make_request(url, "GET", self.headers, not_found_ok=True, **kwargs)
+ return data
+
+ def list_zones(self, name_filter=None, start_from=None, max_items=None, search_by_admin_group=False,
+ ignore_access=False, **kwargs):
"""
Gets a list of zones that currently exist
:return: a list of zones
@@ -522,6 +561,17 @@ class VinylDNSClient(object):
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
return data
+ def get_recordset_count(self, zone_id,**kwargs):
+ """
+ Get count of record set in managed records tab
+ :param zone_id: the zone id the recordset belongs to
+ :return: the value of count
+ """
+ url = urljoin(self.index_url, "/zones/{0}/recordsetcount".format(zone_id))
+
+ response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
+ return data
+
def get_recordset_change(self, zone_id, rs_id, change_id, **kwargs):
"""
Gets an existing recordset change
@@ -535,7 +585,8 @@ class VinylDNSClient(object):
response, data = self.make_request(url, "GET", self.headers, None, not_found_ok=True, **kwargs)
return data
- def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None, record_type_filter=None, name_sort=None, **kwargs):
+ def list_recordsets_by_zone(self, zone_id, start_from=None, max_items=None, record_name_filter=None,
+ record_type_filter=None, name_sort=None, **kwargs):
"""
Retrieves all recordsets in a zone
:param zone_id: the zone to retrieve
@@ -618,7 +669,8 @@ class VinylDNSClient(object):
_, data = self.make_request(url, "POST", self.headers, **kwargs)
return data
- def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None, **kwargs):
+ def list_batch_change_summaries(self, start_from=None, max_items=None, ignore_access=False, approval_status=None,
+ **kwargs):
"""
Gets list of user's batch change summaries
:return: the content of the response
@@ -660,7 +712,8 @@ class VinylDNSClient(object):
:return: the content of the response
"""
url = urljoin(self.index_url, "/zones/{0}/acl/rules".format(zone_id))
- response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request, **kwargs)
+ response, data = self.make_request(url, "PUT", self.headers, json.dumps(acl_rule), sign_request=sign_request,
+ **kwargs)
return data
@@ -804,16 +857,11 @@ class VinylDNSClient(object):
while change["status"] != expected_status and retries > 0:
time.sleep(RETRY_WAIT)
retries -= 1
- latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"], change["id"], status=(200, 404))
+ latest_change = self.get_recordset_change(change["recordSet"]["zoneId"], change["recordSet"]["id"],
+ change["id"], status=(200, 404))
if type(latest_change) != str:
change = latest_change
- if change["status"] != expected_status:
- print("Failed waiting for record change status")
- print(json.dumps(change, indent=3))
- if "systemMessage" in change:
- print("systemMessage is " + change["systemMessage"])
-
assert_that(change["status"], is_(expected_status))
return change
@@ -838,10 +886,6 @@ class VinylDNSClient(object):
else:
change = latest_change
- if not self.batch_is_completed(change):
- print("Failed waiting for record change status")
- print(change)
-
assert_that(self.batch_is_completed(change), is_(True))
return change
diff --git a/modules/api/src/test/resources/application.conf b/modules/api/src/test/resources/application.conf
index 962b2bbcb..93f88e24e 100644
--- a/modules/api/src/test/resources/application.conf
+++ b/modules/api/src/test/resources/application.conf
@@ -127,7 +127,10 @@ vinyldns {
from = ${?EMAIL_FROM}
}
}
-
+ valid-email-config{
+ email-domains = ["test.com","*dummy.com"]
+ number-of-dots= 2
+ }
sns {
class-name = "vinyldns.apadi.notifier.sns.SnsNotifierProvider"
class-name = ${?SNS_CLASS_NAME}
diff --git a/modules/api/src/test/scala/vinyldns/api/CatsHelpers.scala b/modules/api/src/test/scala/vinyldns/api/CatsHelpers.scala
index c3608c754..6064531e4 100644
--- a/modules/api/src/test/scala/vinyldns/api/CatsHelpers.scala
+++ b/modules/api/src/test/scala/vinyldns/api/CatsHelpers.scala
@@ -16,46 +16,14 @@
package vinyldns.api
-import cats.effect._
import cats.implicits._
import vinyldns.api.domain.batch.BatchChangeInterfaces.ValidatedBatch
import vinyldns.api.domain.batch.BatchTransformations.ChangeForValidation
-import scala.concurrent.duration._
import org.scalatest.Assertions._
import org.scalatest.matchers.{MatchResult, Matcher}
-import scala.concurrent.ExecutionContext
-
trait CatsHelpers {
- private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
- private implicit val cs: ContextShift[IO] =
- IO.contextShift(scala.concurrent.ExecutionContext.global)
-
- def await[E, T](f: => IO[T], duration: FiniteDuration = 60.seconds): T = {
- val i: IO[Either[E, T]] = f.attempt.map {
- case Right(ok) => Right(ok.asInstanceOf[T])
- case Left(e) => Left(e.asInstanceOf[E])
- }
- awaitResultOf[E, T](i, duration).toOption.get
- }
-
- // Waits for the future to complete, then returns the value as an Either[Throwable, T]
- def awaitResultOf[E, T](
- f: => IO[Either[E, T]],
- duration: FiniteDuration = 60.seconds
- ): Either[E, T] = {
- val timeOut = IO.sleep(duration) *> IO(new RuntimeException("Timed out waiting for result"))
- IO.race(timeOut, f).unsafeRunSync().toOption.get
- }
-
- // Assumes that the result of the future operation will be successful, this will fail on a left disjunction
- def rightResultOf[E, T](f: => IO[Either[E, T]], duration: FiniteDuration = 60.seconds): T =
- rightValue(awaitResultOf[E, T](f, duration))
-
- // Assumes that the result of the future operation will fail, this will error on a right disjunction
- def leftResultOf[E, T](f: => IO[Either[E, T]], duration: FiniteDuration = 60.seconds): E =
- leftValue(awaitResultOf(f, duration))
def leftValue[E, T](t: Either[E, T]): E = t match {
case Right(x) => fail(s"expected left value, got right: $x")
diff --git a/modules/api/src/test/scala/vinyldns/api/ResultHelpers.scala b/modules/api/src/test/scala/vinyldns/api/ResultHelpers.scala
index 2fe473fb8..30ecf36df 100644
--- a/modules/api/src/test/scala/vinyldns/api/ResultHelpers.scala
+++ b/modules/api/src/test/scala/vinyldns/api/ResultHelpers.scala
@@ -18,54 +18,15 @@ package vinyldns.api
import cats.data.Validated.{Invalid, Valid}
import cats.data.ValidatedNel
-import cats.effect._
-import cats.implicits._
import cats.scalatest.ValidatedMatchers
import org.scalatest.matchers.should.Matchers
import org.scalatest.propspec.AnyPropSpec
-import scala.concurrent.ExecutionContext
-import scala.concurrent.duration._
import scala.reflect.ClassTag
final case class TimeoutException(message: String) extends Throwable(message)
trait ResultHelpers {
- private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
- private implicit val cs: ContextShift[IO] =
- IO.contextShift(scala.concurrent.ExecutionContext.global)
-
- def await[T](f: => IO[_], duration: FiniteDuration = 60.seconds): T =
- awaitResultOf[T](f.map(_.asInstanceOf[T]).attempt, duration).toOption.get
-
- // Waits for the future to complete, then returns the value as an Either[Throwable, T]
- def awaitResultOf[T](
- f: => IO[Either[Throwable, T]],
- duration: FiniteDuration = 60.seconds
- ): Either[Throwable, T] = {
-
- val timeOut = IO.sleep(duration) *> IO(
- TimeoutException("Timed out waiting for result").asInstanceOf[Throwable]
- )
-
- IO.race(timeOut, f.handleError(e => Left(e))).unsafeRunSync() match {
- case Left(e) => Left(e)
- case Right(ok) => ok
- }
- }
-
- // Assumes that the result of the future operation will be successful, this will fail on a left disjunction
- def rightResultOf[T](f: => IO[Either[Throwable, T]], duration: FiniteDuration = 60.seconds): T =
- awaitResultOf[T](f, duration) match {
- case Right(result) => result
- case Left(error) => throw error
- }
-
- // Assumes that the result of the future operation will fail, this will error on a right disjunction
- def leftResultOf[T](
- f: => IO[Either[Throwable, T]],
- duration: FiniteDuration = 60.seconds
- ): Throwable = awaitResultOf(f, duration).swap.toOption.get
def leftValue[T](t: Either[Throwable, T]): Throwable = t.swap.toOption.get
diff --git a/modules/api/src/test/scala/vinyldns/api/VinylDNSTestHelpers.scala b/modules/api/src/test/scala/vinyldns/api/VinylDNSTestHelpers.scala
index d4fc56ba5..86e30d21c 100644
--- a/modules/api/src/test/scala/vinyldns/api/VinylDNSTestHelpers.scala
+++ b/modules/api/src/test/scala/vinyldns/api/VinylDNSTestHelpers.scala
@@ -38,9 +38,9 @@ trait VinylDNSTestHelpers {
val highValueDomainRegexList: List[Regex] = List(new Regex("high-value-domain.*"))
val highValueDomainIpList: List[IpAddress] =
- (IpAddress("192.0.2.252") ++ IpAddress("192.0.2.253") ++ IpAddress(
+ (IpAddress.fromString("192.0.2.252") ++ IpAddress.fromString("192.0.2.253") ++ IpAddress.fromString(
"fd69:27cc:fe91:0:0:0:0:ffff"
- ) ++ IpAddress(
+ ) ++ IpAddress.fromString(
"fd69:27cc:fe91:0:0:0:ffff:0"
)).toList
@@ -58,9 +58,9 @@ trait VinylDNSTestHelpers {
val manualReviewDomainList: List[Regex] = List(new Regex("needs-review.*"))
val manualReviewIpList: List[IpAddress] =
- (IpAddress("192.0.2.254") ++ IpAddress("192.0.2.255") ++ IpAddress(
+ (IpAddress.fromString("192.0.2.254") ++ IpAddress.fromString("192.0.2.255") ++ IpAddress.fromString(
"fd69:27cc:fe91:0:0:0:ffff:1"
- ) ++ IpAddress("fd69:27cc:fe91:0:0:0:ffff:2")).toList
+ ) ++ IpAddress.fromString("fd69:27cc:fe91:0:0:0:ffff:2")).toList
val manualReviewZoneNameList: Set[String] = Set("zone.needs.review.")
@@ -85,7 +85,7 @@ trait VinylDNSTestHelpers {
LimitsConfig(100,100,1000,1500,100,100,100)
val testServerConfig: ServerConfig =
- ServerConfig(100, 100, 100, 100, true, approvedNameServers, "blue", "unset", "vinyldns.", false, true, true)
+ ServerConfig(100, 100, 100, 100, true, approvedNameServers, "blue", "unset", "vinyldns.", false, true, true, true)
val batchChangeConfig: BatchChangeConfig =
BatchChangeConfig(batchChangeLimit, sharedApprovedTypes, v6DiscoveryNibbleBoundaries)
diff --git a/modules/api/src/test/scala/vinyldns/api/backend/dns/DnsBackendSpec.scala b/modules/api/src/test/scala/vinyldns/api/backend/dns/DnsBackendSpec.scala
index cbf65662f..7de828120 100644
--- a/modules/api/src/test/scala/vinyldns/api/backend/dns/DnsBackendSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/backend/dns/DnsBackendSpec.scala
@@ -30,6 +30,7 @@ import org.xbill.DNS
import org.xbill.DNS.{Lookup, Name, TSIG}
import vinyldns.api.backend.dns.DnsProtocol._
import vinyldns.core.crypto.{CryptoAlgebra, NoOpCrypto}
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.backend.BackendResponse
import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.record._
@@ -47,7 +48,7 @@ class DnsBackendSpec
with EitherValues {
private val zoneConnection =
- ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")
+ ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")
private val testZone = Zone("vinyldns", "test@test.com")
private val testA = RecordSet(
testZone.id,
diff --git a/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala b/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala
index 4317fc534..63d4e70e0 100644
--- a/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/config/VinylDNSConfigSpec.scala
@@ -21,6 +21,7 @@ import org.scalatest.BeforeAndAfterAll
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import vinyldns.api.backend.dns.DnsBackendProviderConfig
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.zone.ZoneConnection
import vinyldns.core.repository.RepositoryName._
@@ -68,7 +69,7 @@ class VinylDNSConfigSpec extends AnyWordSpec with Matchers with BeforeAndAfterAl
}
"load specified backends" in {
- val zc = ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"))
+ val zc = ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), sys.env.getOrElse("DEFAULT_DNS_ADDRESS", "127.0.0.1:19001"))
val tc = zc.copy()
val backends = underTest.backendConfigs.backendProviders
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala
index 3101f9571..68dc9535e 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/DomainValidationsSpec.scala
@@ -22,7 +22,7 @@ import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks
import org.scalatest.propspec.AnyPropSpec
import org.scalatest.matchers.should.Matchers
import vinyldns.api.ValidationTestImprovements._
-import vinyldns.core.domain.{InvalidDomainName, InvalidCname, InvalidLength}
+import vinyldns.core.domain.{InvalidDomainName, Fqdn, InvalidCname, InvalidLength}
class DomainValidationsSpec
extends AnyPropSpec
@@ -77,6 +77,54 @@ class DomainValidationsSpec
validateHostName("asterisk.domain*.name.") shouldBe invalid
}
+ property("Shortest fqdn name should be valid") {
+ val fqdn = Fqdn("a.")
+ validateCname(fqdn, false) shouldBe valid
+ }
+
+ property("Ip address in cname should be invalid") {
+ val fqdn = Fqdn("1.2.3.4")
+ validateCname(fqdn, false) shouldBe invalid
+ }
+
+ property("Longest fqdn name should be valid") {
+ val fqdn = Fqdn(("a" * 50 + ".") * 5)
+ validateCname(fqdn, false) shouldBe valid
+ }
+
+ property("fqdn name should pass property-based testing") {
+ forAll(domainGenerator) { domain: String =>
+ val domains= Fqdn(domain)
+ whenever(validateHostName(domains).isValid) {
+ domains.fqdn.length should be > 0
+ domains.fqdn.length should be < 256
+ (domains.fqdn should fullyMatch).regex(validFQDNRegex)
+ domains.fqdn should endWith(".")
+ }
+ }
+ }
+
+ property("fqdn names beginning with invalid characters should fail with InvalidCname") {
+ validateCname(Fqdn("/slash.domain.name."), false).failWith[InvalidCname]
+ validateCname(Fqdn("-hyphen.domain.name."), false).failWith[InvalidCname]
+ }
+
+ property("fqdn names with underscores should pass property-based testing") {
+ validateCname(Fqdn("_underscore.domain.name."), false).isValid
+ validateCname(Fqdn("under_score.domain.name."), false).isValid
+ validateCname(Fqdn("underscore._domain.name."), false).isValid
+ }
+
+ // For wildcard records. '*' can only be in the beginning followed by '.' and domain name
+ property("fqdn names beginning with asterisk should pass property-based testing") {
+ validateCname(Fqdn("*.domain.name."), false) shouldBe valid
+ validateCname(Fqdn("aste*risk.domain.name."),false) shouldBe invalid
+ validateCname(Fqdn("*asterisk.domain.name."),false) shouldBe invalid
+ validateCname(Fqdn("asterisk*.domain.name."),false) shouldBe invalid
+ validateCname(Fqdn("asterisk.*domain.name."),false)shouldBe invalid
+ validateCname(Fqdn("asterisk.domain*.name."),false) shouldBe invalid
+ }
+
property("Valid Ipv4 addresses should pass property-based testing") {
forAll(validIpv4Gen) { validIp: String =>
val res = validateIpv4Address(validIp)
@@ -112,50 +160,50 @@ class DomainValidationsSpec
}
property("Shortest cname should be valid") {
- validateCname("a.",true) shouldBe valid
- validateCname("a.",false) shouldBe valid
+ validateIsReverseCname("a.",true) shouldBe valid
+ validateIsReverseCname("a.",false) shouldBe valid
}
property("Longest cname should be valid") {
val name = ("a" * 50 + ".") * 5
- validateCname(name,true) shouldBe valid
- validateCname(name,false) shouldBe valid
+ validateIsReverseCname(name,true) shouldBe valid
+ validateIsReverseCname(name,false) shouldBe valid
}
property("Cnames with underscores should pass property-based testing") {
- validateCname("_underscore.domain.name.",true).isValid
- validateCname("under_score.domain.name.",true).isValid
- validateCname("underscore._domain.name.",true).isValid
- validateCname("_underscore.domain.name.",false).isValid
- validateCname("under_score.domain.name.",false).isValid
- validateCname("underscore._domain.name.",false).isValid
+ validateIsReverseCname("_underscore.domain.name.",true).isValid
+ validateIsReverseCname("under_score.domain.name.",true).isValid
+ validateIsReverseCname("underscore._domain.name.",true).isValid
+ validateIsReverseCname("_underscore.domain.name.",false).isValid
+ validateIsReverseCname("under_score.domain.name.",false).isValid
+ validateIsReverseCname("underscore._domain.name.",false).isValid
}
// For wildcard records. '*' can only be in the beginning followed by '.' and domain name
property("Cnames beginning with asterisk should pass property-based testing") {
- validateCname("*.domain.name.",true) shouldBe valid
- validateCname("aste*risk.domain.name.",true) shouldBe invalid
- validateCname("*asterisk.domain.name.",true) shouldBe invalid
- validateCname("asterisk*.domain.name.",true) shouldBe invalid
- validateCname("asterisk.*domain.name.",true) shouldBe invalid
- validateCname("asterisk.domain*.name.",true) shouldBe invalid
- validateCname("*.domain.name.",false) shouldBe valid
- validateCname("aste*risk.domain.name.",false) shouldBe invalid
- validateCname("*asterisk.domain.name.",false) shouldBe invalid
- validateCname("asterisk*.domain.name.",false) shouldBe invalid
- validateCname("asterisk.*domain.name.",false) shouldBe invalid
- validateCname("asterisk.domain*.name.",false) shouldBe invalid
+ validateIsReverseCname("*.domain.name.",true) shouldBe valid
+ validateIsReverseCname("aste*risk.domain.name.",true) shouldBe invalid
+ validateIsReverseCname("*asterisk.domain.name.",true) shouldBe invalid
+ validateIsReverseCname("asterisk*.domain.name.",true) shouldBe invalid
+ validateIsReverseCname("asterisk.*domain.name.",true) shouldBe invalid
+ validateIsReverseCname("asterisk.domain*.name.",true) shouldBe invalid
+ validateIsReverseCname("*.domain.name.",false) shouldBe valid
+ validateIsReverseCname("aste*risk.domain.name.",false) shouldBe invalid
+ validateIsReverseCname("*asterisk.domain.name.",false) shouldBe invalid
+ validateIsReverseCname("asterisk*.domain.name.",false) shouldBe invalid
+ validateIsReverseCname("asterisk.*domain.name.",false) shouldBe invalid
+ validateIsReverseCname("asterisk.domain*.name.",false) shouldBe invalid
}
property("Cname names with forward slash should pass with reverse zone") {
- validateCname("/slash.cname.name.",true).isValid
- validateCname("slash./cname.name.",true).isValid
- validateCname("slash.cname./name.",true).isValid
+ validateIsReverseCname("/slash.cname.name.",true).isValid
+ validateIsReverseCname("slash./cname.name.",true).isValid
+ validateIsReverseCname("slash.cname./name.",true).isValid
}
property("Cname names with forward slash should fail with forward zone") {
- validateCname("/slash.cname.name.",false).failWith[InvalidCname]
- validateCname("slash./cname.name.",false).failWith[InvalidCname]
- validateCname("slash.cname./name.",false).failWith[InvalidCname]
+ validateIsReverseCname("/slash.cname.name.",false).failWith[InvalidCname]
+ validateIsReverseCname("slash./cname.name.",false).failWith[InvalidCname]
+ validateIsReverseCname("slash.cname./name.",false).failWith[InvalidCname]
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/ReverseZoneHelpersSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/ReverseZoneHelpersSpec.scala
index 30bbb3e09..7c35e5bd5 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/ReverseZoneHelpersSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/ReverseZoneHelpersSpec.scala
@@ -137,8 +137,8 @@ class ReverseZoneHelpersSpec
}
"recordsetIsWithinCidrMask" should {
"when testing IPv4" should {
- "filter in/out record set based on CIDR rule of 0 (lower bound for ip4 CIDR rules)" in {
- val mask = "120.1.2.0/0"
+ "filter in/out record set based on CIDR rule of 1 (lower bound for ip4 CIDR rules)" in {
+ val mask = "120.1.2.0/1"
val znTrue = Zone("40.120.in-addr.arpa.", "email")
val rsTrue =
RecordSet("id", "20.3", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
@@ -151,7 +151,7 @@ class ReverseZoneHelpersSpec
}
"filter in/out record set based on CIDR rule of 8" in {
- val mask = "10.10.32/19"
+ val mask = "10.10.32.0/19"
val zone = Zone("10.10.in-addr.arpa.", "email")
val recordSet =
RecordSet("id", "90.44", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/access/AccessValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/access/AccessValidationsSpec.scala
index d32f41055..9a14ce884 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/access/AccessValidationsSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/access/AccessValidationsSpec.scala
@@ -26,7 +26,7 @@ import vinyldns.api.domain.zone.{NotAuthorizedError, RecordSetInfo, RecordSetLis
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestRecordSetData._
import vinyldns.core.TestZoneData._
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership.User
import vinyldns.core.domain.record._
@@ -95,7 +95,7 @@ class AccessValidationsSpec
VinylDNSTestHelpers.sharedApprovedTypes
)
- private val testUser = User("test", "test", "test", isTest = true)
+ private val testUser = User("test", "test", Encrypted("test"), isTest = true)
"canSeeZone" should {
"return a NotAuthorizedError if the user is not admin or super user with no acl rules" in {
@@ -131,6 +131,41 @@ class AccessValidationsSpec
}
}
+ "canSeeZoneChange" should {
+ "return a NotAuthorizedError if the user is not admin or super user with no acl rules" in {
+ val error = leftValue(accessValidationTest.canSeeZoneChange(okAuth, zoneNotAuthorized))
+ error shouldBe a[NotAuthorizedError]
+ }
+
+ "return true if the user is an admin or super user" in {
+ val auth = okAuth.copy(
+ signedInUser = okAuth.signedInUser.copy(isSuper = true),
+ memberGroupIds = Seq.empty
+ )
+ accessValidationTest.canSeeZoneChange(auth, okZone) should be(right)
+ }
+
+ "return false if there is an acl rule for the user in the zone" in {
+ val rule = ACLRule(AccessLevel.Read, userId = Some(okAuth.userId))
+ val zoneIn = zoneNotAuthorized.copy(acl = ZoneACL(Set(rule)))
+
+ val error = leftValue(accessValidationTest.canSeeZoneChange(okAuth, zoneIn))
+ error shouldBe a[NotAuthorizedError]
+ }
+
+ "return true if the user is a support admin" in {
+ val supportAuth = okAuth.copy(
+ signedInUser = okAuth.signedInUser.copy(isSupport = true),
+ memberGroupIds = Seq.empty
+ )
+ accessValidationTest.canSeeZone(supportAuth, okZone) should be(right)
+ }
+
+ "return true if the zone is shared and user does not have other access" in {
+ accessValidationTest.canSeeZone(okAuth, sharedZone) should be(right)
+ }
+ }
+
"canChangeZone" should {
"return a NotAuthorizedError if the user is not admin or super user" in {
val error = leftValue(
@@ -255,6 +290,7 @@ class AccessValidationsSpec
) should be(right)
}
}
+
"canUpdateRecordSet" should {
"return a NotAuthorizedError if the user has AccessLevel.NoAccess" in {
val error = leftValue(
@@ -292,6 +328,13 @@ class AccessValidationsSpec
)
}
+ "return true if the user has AccessLevel.Read or AccessLevel.NoAccess and superUserCanUpdateOwnerGroup is true" in {
+ accessValidationTest.canUpdateRecordSet(userAuthRead, "test", RecordType.A, zoneInRead,
+ None, superUserCanUpdateOwnerGroup = true) should be(right)
+ accessValidationTest.canUpdateRecordSet(userAuthNone, "test", RecordType.A, zoneInNone,
+ None, superUserCanUpdateOwnerGroup = true) should be(right)
+ }
+
"return true if the user is in the owner group and the zone is shared" in {
val zone = okZone.copy(shared = true)
val record = aaaa.copy(zoneId = zone.id, ownerGroupId = Some(oneUserDummyGroup.id))
@@ -365,7 +408,7 @@ class AccessValidationsSpec
RecordType.PTR,
zoneIp4,
None,
- List(PTRData(Fqdn("test.foo.comcast.net")))
+ newRecordData = List(PTRData(Fqdn("test.foo.comcast.net")))
) should be(right)
}
}
@@ -957,8 +1000,8 @@ class AccessValidationsSpec
"ruleAppliesToRecordNameIPv4" should {
- "filter in/out record set based on CIDR rule of 0 (lower bound for ip4 CIDR rules)" in {
- val aclRule = userReadAcl.copy(recordMask = Some("120.1.2.0/0"))
+ "filter in/out record set based on CIDR rule of 1 (lower bound for ip4 CIDR rules)" in {
+ val aclRule = userReadAcl.copy(recordMask = Some("120.1.2.0/1"))
val znTrue = Zone("40.120.in-addr.arpa.", "email")
val rsTrue =
RecordSet("id", "20.3", RecordType.PTR, 200, RecordSetStatus.Active, Instant.now.truncatedTo(ChronoUnit.MILLIS))
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/auth/MembershipAuthPrincipalProviderSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/auth/MembershipAuthPrincipalProviderSpec.scala
index 6558c1c69..fb5c66c8a 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/auth/MembershipAuthPrincipalProviderSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/auth/MembershipAuthPrincipalProviderSpec.scala
@@ -21,17 +21,14 @@ import org.mockito.Mockito._
import org.scalatestplus.mockito.MockitoSugar
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
-import vinyldns.api.ResultHelpers
import vinyldns.core.TestMembershipData._
import vinyldns.core.domain.membership.{MembershipRepository, UserRepository}
import cats.effect._
-import vinyldns.core.domain.auth.AuthPrincipal
class MembershipAuthPrincipalProviderSpec
extends AnyWordSpec
with Matchers
- with MockitoSugar
- with ResultHelpers {
+ with MockitoSugar {
"MembershipAuthPrincipalProvider" should {
"return the AuthPrincipal" in {
@@ -65,7 +62,7 @@ class MembershipAuthPrincipalProviderSpec
.when(mockUserRepo)
.getUserByAccessKey(any[String])
- val result = await[Option[AuthPrincipal]](underTest.getAuthPrincipal("None"))
+ val result = underTest.getAuthPrincipal("None").unsafeRunSync()
result shouldBe None
}
"return an empty list of groups if there are no matching groups" in {
@@ -83,7 +80,7 @@ class MembershipAuthPrincipalProviderSpec
.when(mockMembershipRepo)
.getGroupsForUser(any[String])
- val result = await[Option[AuthPrincipal]](underTest.getAuthPrincipal(accessKey))
+ val result = underTest.getAuthPrincipal(accessKey).unsafeRunSync()
result.map { authPrincipal =>
authPrincipal.signedInUser shouldBe okUser
authPrincipal.memberGroupIds shouldBe Seq()
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeConverterSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeConverterSpec.scala
index 8a46c66fc..16ca713c3 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeConverterSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeConverterSpec.scala
@@ -22,11 +22,11 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
-import vinyldns.api.CatsHelpers
import vinyldns.api.domain.batch.BatchTransformations._
import vinyldns.api.domain.batch.BatchTransformations.LogicalChangeType._
import vinyldns.api.engine.TestMessageQueue
import vinyldns.api.repository._
+import vinyldns.core.Messages.{nonExistentRecordDataDeleteMessage, nonExistentRecordDeleteMessage}
import vinyldns.core.TestMembershipData.okUser
import vinyldns.core.TestRecordSetData._
import vinyldns.core.TestZoneData.{okZone, _}
@@ -37,9 +37,7 @@ import vinyldns.core.domain.record.RecordType.{RecordType, _}
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.Zone
-class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelpers {
- private val notExistCompletedMessage: String = "This record does not exist." +
- "No further action is required."
+class BatchChangeConverterSpec extends AnyWordSpec with Matchers {
private def makeSingleAddChange(
name: String,
@@ -63,7 +61,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
)
}
- private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone) = {
+ private def makeSingleDeleteRRSetChange(name: String, typ: RecordType, zone: Zone = okZone, systemMessage: Option[String] = None) = {
val fqdn = s"$name.${zone.name}"
SingleDeleteRRSetChange(
Some(zone.id),
@@ -73,7 +71,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
typ,
None,
SingleChangeStatus.Pending,
- None,
+ systemMessage,
None,
None
)
@@ -87,18 +85,19 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
AddChangeForValidation(
okZone,
s"$recordName",
- AddChangeInput(s"$recordName.ok.", typ, Some(123), recordData),
+ AddChangeInput(s"$recordName.ok.", typ, None, Some(123), recordData),
7200L
)
private def makeDeleteRRSetChangeForValidation(
recordName: String,
- typ: RecordType = RecordType.A
+ typ: RecordType = RecordType.A,
+ systemMessage: Option[String] = None
): DeleteRRSetChangeForValidation =
DeleteRRSetChangeForValidation(
okZone,
s"$recordName",
- DeleteRRSetChangeInput(s"$recordName.ok.", typ)
+ DeleteRRSetChangeInput(s"$recordName.ok.", typ, systemMessage, None)
)
private val addSingleChangesGood = List(
@@ -164,11 +163,19 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
)
private val singleChangesOneDelete = List(
- makeSingleDeleteRRSetChange("DoesNotExistToDelete", A)
+ makeSingleDeleteRRSetChange("DoesNotExistToDelete", A, okZone, Some(nonExistentRecordDeleteMessage))
)
private val changeForValidationOneDelete = List(
- makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A)
+ makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A, Some(nonExistentRecordDeleteMessage))
+ )
+
+ private val singleChangesOneDataDelete = List(
+ makeSingleDeleteRRSetChange("DataDoesNotExistToDelete", A, okZone, Some(nonExistentRecordDataDeleteMessage))
+ )
+
+ private val changeForValidationOneDataDelete = List(
+ makeDeleteRRSetChangeForValidation("DataDoesNotExistToDelete", A, Some(nonExistentRecordDataDeleteMessage))
)
private val singleChangesOneBad = List(
@@ -305,7 +312,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
addSingleChangesGood,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChange,
@@ -313,8 +320,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
ChangeForValidationMap(addChangeForValidationGood.map(_.validNel), existingRecordSets),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
val rsChanges = result.recordSetChanges
// validate recordset changes generated from batch
@@ -339,7 +346,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
deleteSingleChangesGood,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChange,
@@ -350,8 +357,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
val rsChanges = result.recordSetChanges
// validate recordset change basics generated from batch
@@ -388,7 +395,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
updateSingleChangesGood,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChange,
@@ -399,8 +406,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
val rsChanges = result.recordSetChanges
// validate recordset changes generated from batch
@@ -435,7 +442,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
changes,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChange,
@@ -443,8 +450,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
ChangeForValidationMap(changeForValidation.map(_.validNel), existingRecordSets),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
val rsChanges = result.recordSetChanges
// validate recordset changes generated from batch
@@ -471,7 +478,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
// check the batch has been stored in the DB
val savedBatch: Option[BatchChange] =
- await(batchChangeRepo.getBatchChange(batchChange.id))
+ batchChangeRepo.getBatchChange(batchChange.id).unsafeRunSync()
savedBatch shouldBe Some(batchChange)
}
@@ -486,7 +493,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
List(),
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChange,
@@ -494,8 +501,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
ChangeForValidationMap(List(), existingRecordSets),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.batchChange shouldBe batchChange
}
@@ -510,7 +517,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
singleChangesOneBad,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchWithBadChange,
@@ -518,8 +525,8 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
ChangeForValidationMap(changeForValidationOneBad.map(_.validNel), existingRecordSets),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
val rsChanges = result.recordSetChanges
rsChanges.length shouldBe 3
@@ -542,11 +549,11 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
// check the update has been made in the DB
val savedBatch: Option[BatchChange] =
- await(batchChangeRepo.getBatchChange(batchWithBadChange.id))
+ batchChangeRepo.getBatchChange(batchWithBadChange.id).unsafeRunSync()
savedBatch shouldBe Some(returnedBatch)
}
- "set status to complete when deleting a record that does not exist" in {
+ "set status to pending when deleting a record that does not exist" in {
val batchWithBadChange =
BatchChange(
okUser.id,
@@ -556,7 +563,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
singleChangesOneDelete,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchWithBadChange,
@@ -564,21 +571,55 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
ChangeForValidationMap(changeForValidationOneDelete.map(_.validNel), existingRecordSets),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
val returnedBatch = result.batchChange
// validate completed status returned
val receivedChange = returnedBatch.changes(0)
- receivedChange.status shouldBe SingleChangeStatus.Complete
+ receivedChange.status shouldBe SingleChangeStatus.Pending
receivedChange.recordChangeId shouldBe None
- receivedChange.systemMessage shouldBe Some(notExistCompletedMessage)
- returnedBatch.changes(0) shouldBe singleChangesOneDelete(0).copy(systemMessage = Some(notExistCompletedMessage), status = SingleChangeStatus.Complete)
+ receivedChange.systemMessage shouldBe Some(nonExistentRecordDeleteMessage)
+ returnedBatch.changes(0) shouldBe singleChangesOneDelete(0).copy(systemMessage = Some(nonExistentRecordDeleteMessage), status = SingleChangeStatus.Pending)
// check the update has been made in the DB
val savedBatch: Option[BatchChange] =
- await(batchChangeRepo.getBatchChange(batchWithBadChange.id))
+ batchChangeRepo.getBatchChange(batchWithBadChange.id).unsafeRunSync()
+ savedBatch shouldBe Some(returnedBatch)
+ }
+
+ "set status to pending when deleting a record data that does not exist" in {
+ val batchWithBadChange =
+ BatchChange(
+ okUser.id,
+ okUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ singleChangesOneDataDelete,
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved
+ )
+ val result =
+ underTest
+ .sendBatchForProcessing(
+ batchWithBadChange,
+ existingZones,
+ ChangeForValidationMap(changeForValidationOneDataDelete.map(_.validNel), existingRecordSets),
+ None
+ )
+ .value.unsafeRunSync().toOption.get
+
+ val returnedBatch = result.batchChange
+
+ // validate completed status returned
+ val receivedChange = returnedBatch.changes(0)
+ receivedChange.status shouldBe SingleChangeStatus.Pending
+ receivedChange.recordChangeId shouldBe None
+ receivedChange.systemMessage shouldBe Some(nonExistentRecordDataDeleteMessage)
+ returnedBatch.changes(0) shouldBe singleChangesOneDataDelete(0).copy(systemMessage = Some(nonExistentRecordDataDeleteMessage), status = SingleChangeStatus.Pending)
+
+ // check the update has been made in the DB
+ val savedBatch: Option[BatchChange] =
+ batchChangeRepo.getBatchChange(batchWithBadChange.id).unsafeRunSync()
savedBatch shouldBe Some(returnedBatch)
}
@@ -592,7 +633,7 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
singleChangesOneUnsupported,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = leftResultOf(
+ val result =
underTest
.sendBatchForProcessing(
batchChangeUnsupported,
@@ -603,12 +644,12 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
),
None
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
result shouldBe an[BatchConversionError]
val notSaved: Option[BatchChange] =
- await(batchChangeRepo.getBatchChange(batchChangeUnsupported.id))
+ batchChangeRepo.getBatchChange(batchChangeUnsupported.id).unsafeRunSync()
notSaved shouldBe None
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInputSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInputSpec.scala
index fb614e387..b7d03b4fc 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInputSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInputSpec.scala
@@ -31,16 +31,16 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
"BatchChangeInput" should {
"ensure trailing dot on A, AAAA, and CNAME fqdn" in {
- val changeA = AddChangeInput("apex.test.com", A, Some(100), AData("1.1.1.1"))
+ val changeA = AddChangeInput("apex.test.com", A, None, Some(100), AData("1.1.1.1"))
val changeAAAA =
- AddChangeInput("aaaa.test.com", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput("aaaa.test.com", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
val changeCname =
- AddChangeInput("cname.test.com", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com")))
- val changeADotted = AddChangeInput("adot.test.com.", A, Some(100), AData("1.1.1.1"))
+ AddChangeInput("cname.test.com", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com")))
+ val changeADotted = AddChangeInput("adot.test.com.", A, None, Some(100), AData("1.1.1.1"))
val changeAAAADotted =
- AddChangeInput("aaaadot.test.com.", AAAA, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput("aaaadot.test.com.", AAAA, None, Some(3600), AAAAData("1:2:3:4:5:6:7:8"))
val changeCnameDotted =
- AddChangeInput("cnamedot.test.com.", CNAME, Some(100), CNAMEData(Fqdn("testing.test.com.")))
+ AddChangeInput("cnamedot.test.com.", CNAME, None, Some(100), CNAMEData(Fqdn("testing.test.com.")))
val input = BatchChangeInput(
None,
@@ -58,7 +58,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
}
"asNewStoredChange" should {
"Convert an AddChangeInput into SingleAddChange" in {
- val changeA = AddChangeInput("some.test.com", A, None, AData("1.1.1.1"))
+ val changeA = AddChangeInput("some.test.com", A, None, None, AData("1.1.1.1"))
val converted = changeA.asNewStoredChange(
NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl
@@ -80,7 +80,7 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
asAdd.recordSetId shouldBe None
}
"Convert a DeleteChangeInput into SingleDeleteRRSetChange" in {
- val changeA = DeleteRRSetChangeInput("some.test.com", A)
+ val changeA = DeleteRRSetChangeInput("some.test.com", A, None)
val converted = changeA.asNewStoredChange(
NonEmptyList.of(ZoneDiscoveryError("test")),
VinylDNSTestHelpers.defaultTtl
@@ -111,14 +111,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
1234,
AData("1.2.3.4"),
SingleChangeStatus.NeedsReview,
- Some("msg"),
+ None,
None,
None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
)
val expectedAddChange =
- AddChangeInput("testRname.testZoneName.", A, Some(1234), AData("1.2.3.4"))
+ AddChangeInput("testRname.testZoneName.", A, None, Some(1234), AData("1.2.3.4"))
val singleDelChange = SingleDeleteRRSetChange(
Some("testZoneId"),
@@ -128,14 +128,14 @@ class BatchChangeInputSpec extends AnyWordSpec with Matchers {
A,
None,
SingleChangeStatus.NeedsReview,
- Some("msg"),
+ None,
None,
None,
List(SingleChangeError(DomainValidationErrorType.ZoneDiscoveryError, "test err"))
)
val expectedDelChange =
- DeleteRRSetChangeInput("testRname.testZoneName.", A)
+ DeleteRRSetChangeInput("testRname.testZoneName.", A, None)
val change = BatchChange(
"userId",
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInterfacesSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInterfacesSpec.scala
index fffcd46dd..ec9f21bb9 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInterfacesSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeInterfacesSpec.scala
@@ -18,26 +18,25 @@ package vinyldns.api.domain.batch
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
-import vinyldns.api.CatsHelpers
import vinyldns.api.domain.batch.BatchChangeInterfaces._
import cats.effect._
import cats.implicits._
import vinyldns.core.domain.{BatchChangeIsEmpty, ChangeLimitExceeded}
-class BatchChangeInterfacesSpec extends AnyWordSpec with Matchers with CatsHelpers {
+class BatchChangeInterfacesSpec extends AnyWordSpec with Matchers {
"toBatchResult" should {
"work with either success input" in {
val input = "good"
val out = input.asRight[BatchChangeErrorResponse].toBatchResult
- rightResultOf(out.value) shouldBe input
+ out.value.unsafeRunSync().toOption.get shouldBe input
}
"work with either failure input" in {
val error = InvalidBatchChangeInput(List(BatchChangeIsEmpty(10)))
val out = error.asLeft.toBatchResult
- leftResultOf(out.value) shouldBe error
+ out.value.unsafeRunSync().swap.toOption.get shouldBe error
}
"work with Future success inputs" in {
val input = "good"
@@ -49,42 +48,42 @@ class BatchChangeInterfacesSpec extends AnyWordSpec with Matchers with CatsHelpe
val out2 = futureEitherA.toBatchResult
val out3 = futureEitherANoType.toBatchResult
- rightResultOf(out1.value) shouldBe input
- rightResultOf(out2.value) shouldBe input
- rightResultOf(out3.value) shouldBe input
+ out1.value.unsafeRunSync().toOption.get shouldBe input
+ out2.value.unsafeRunSync().toOption.get shouldBe input
+ out3.value.unsafeRunSync().toOption.get shouldBe input
}
"return a BatchChangeIsEmpty error if no changes are found" in {
val futureError =
IO.pure(InvalidBatchChangeInput(List(BatchChangeIsEmpty(10))).asLeft)
val output = futureError.toBatchResult
- leftResultOf(output.value) shouldBe InvalidBatchChangeInput(List(BatchChangeIsEmpty(10)))
+ output.value.unsafeRunSync().swap.toOption.get shouldBe InvalidBatchChangeInput(List(BatchChangeIsEmpty(10)))
}
"return a ChangeLimitExceeded error if change limit is exceeded" in {
val futureError =
IO.pure(InvalidBatchChangeInput(List(ChangeLimitExceeded(10))).asLeft)
val output = futureError.toBatchResult
- leftResultOf(output.value) shouldBe InvalidBatchChangeInput(List(ChangeLimitExceeded(10)))
+ output.value.unsafeRunSync().swap.toOption.get shouldBe InvalidBatchChangeInput(List(ChangeLimitExceeded(10)))
}
"return a UnknownConversionError if run-time error is encountered during processing" in {
val futureError = IO.pure(new RuntimeException("bad!").asLeft)
val output = futureError.toBatchResult
- leftResultOf(output.value) shouldBe an[UnknownConversionError]
+ output.value.unsafeRunSync().swap.toOption.get shouldBe an[UnknownConversionError]
}
"return a RuntimeException error if Future fails" in {
val futureError = IO.raiseError(new RuntimeException("bad!"))
val output = futureError.toBatchResult
- a[RuntimeException] shouldBe thrownBy(await(output.value))
+ a[RuntimeException] shouldBe thrownBy(output.value.unsafeRunSync())
}
}
"collectSuccesses" should {
"return a IO[List] of all if all are successful" in {
val futures = List(1, 2, 3, 4).map(IO.pure)
- val result = await(futures.collectSuccesses)
+ val result = futures.collectSuccesses.unsafeRunSync()
result shouldBe List(1, 2, 3, 4)
}
"filter out unsuccessful futures" in {
@@ -96,7 +95,7 @@ class BatchChangeInterfacesSpec extends AnyWordSpec with Matchers with CatsHelpe
IO.pure(3)
)
- val result = await(futures.collectSuccesses)
+ val result = futures.collectSuccesses.unsafeRunSync()
result shouldBe List(1, 2, 3)
}
"return an empty list of all fail" in {
@@ -105,7 +104,7 @@ class BatchChangeInterfacesSpec extends AnyWordSpec with Matchers with CatsHelpe
IO.raiseError(new RuntimeException("bad again"))
)
- val result = await(futures.collectSuccesses)
+ val result = futures.collectSuccesses.unsafeRunSync()
result shouldBe List()
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeServiceSpec.scala
index e184c8c6b..bf79fd38e 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeServiceSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeServiceSpec.scala
@@ -21,13 +21,12 @@ import cats.effect._
import cats.implicits._
import cats.scalatest.{EitherMatchers, ValidatedMatchers}
import java.time.temporal.ChronoUnit
-import java.time.Instant
+import java.time.{Instant, LocalDateTime, ZoneId}
import org.scalatestplus.mockito.MockitoSugar
import org.scalatest.{BeforeAndAfterEach, EitherValues}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import vinyldns.api.ValidatedBatchMatcherImprovements.containChangeForValidation
-import vinyldns.api._
import vinyldns.api.domain.auth.AuthPrincipalProvider
import vinyldns.api.domain.batch.BatchChangeInterfaces.{BatchResult, _}
import vinyldns.api.domain.batch.BatchTransformations._
@@ -53,13 +52,13 @@ import org.mockito.Mockito._
import vinyldns.api.VinylDNSTestHelpers
import vinyldns.api.domain.access.AccessValidations
+import java.time.format.DateTimeFormatter
import scala.concurrent.ExecutionContext
class BatchChangeServiceSpec
extends AnyWordSpec
with Matchers
with MockitoSugar
- with CatsHelpers
with BeforeAndAfterEach
with EitherMatchers
with EitherValues
@@ -68,7 +67,7 @@ class BatchChangeServiceSpec
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private val nonFatalErrorZoneDiscoveryError = ZoneDiscoveryError("test")
- private val nonFatalErrorRecordAlreadyExists = RecordAlreadyExists("test", AData("1.1.1.1"), true)
+ private val nonFatalErrorRecordAlreadyExists = RecordAlreadyExists("test")
private val validations = new BatchChangeValidations(
new AccessValidations(
@@ -77,38 +76,41 @@ class BatchChangeServiceSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.manualReviewConfig,
VinylDNSTestHelpers.batchChangeConfig,
- VinylDNSTestHelpers.scheduledChangesConfig
+ VinylDNSTestHelpers.scheduledChangesConfig,
+ VinylDNSTestHelpers.approvedNameServers
)
private val ttl = Some(200L)
- private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
+ private val apexAddA = AddChangeInput("apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val nonApexAddA =
- AddChangeInput("non-apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("non-apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val onlyApexAddA =
- AddChangeInput("only.apex.exists", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("only.apex.exists", RecordType.A, None, ttl, AData("1.1.1.1"))
private val onlyBaseAddAAAA =
- AddChangeInput("have.only.base", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
- private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("have.only.base", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
+ private val noZoneAddA = AddChangeInput("no.zone.match.", RecordType.A, None, ttl, AData("1.1.1.1"))
private val dottedAddA =
- AddChangeInput("dot.ted.apex.test.com", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("dot.ted.apex.test.com", RecordType.A, None, ttl, AData("1.1.1.1"))
private val cnameAdd =
- AddChangeInput("cname.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com.")))
+ AddChangeInput("cname.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
private val cnameApexAdd =
- AddChangeInput("apex.test.com", RecordType.CNAME, ttl, CNAMEData(Fqdn("testing.test.com.")))
+ AddChangeInput("apex.test.com", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("testing.test.com.")))
private val cnameReverseAdd = AddChangeInput(
"cname.55.144.10.in-addr.arpa",
RecordType.CNAME,
+ None,
ttl,
CNAMEData(Fqdn("testing.cname.com."))
)
- private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
- private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
+ private val ptrAdd = AddChangeInput("10.144.55.11", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
+ private val ptrAdd2 = AddChangeInput("10.144.55.255", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
private val ptrDelegatedAdd =
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
private val ptrV6Add =
AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR,
+ None,
ttl,
PTRData(Fqdn("ptr"))
)
@@ -308,6 +310,10 @@ class BatchChangeServiceSpec
}
object AlwaysExistsZoneRepo extends EmptyZoneRepo {
+
+ override def getAllZonesWithSyncSchedule: IO[Set[Zone]] =
+ IO.pure(Set(Zone("dummyZone", "test@test.com")))
+
override def getZones(zoneIds: Set[String]): IO[Set[Zone]] = {
val zones = zoneIds.map(Zone(_, "test@test.com"))
IO.pure(zones)
@@ -355,6 +361,9 @@ class BatchChangeServiceSpec
ipv6PTR18Zone
)
+ override def getAllZonesWithSyncSchedule: IO[Set[Zone]] =
+ IO.pure(dbZones.filter(zn => zn.recurrenceSchedule.isDefined))
+
override def getZones(zoneIds: Set[String]): IO[Set[Zone]] =
IO.pure(dbZones.filter(zn => zoneIds.contains(zn.id)))
@@ -458,7 +467,7 @@ class BatchChangeServiceSpec
"succeed if all inputs are good" in {
val input = BatchChangeInput(None, List(apexAddA, nonApexAddA))
- val result = rightResultOf(underTest.applyBatchChange(input, auth, true).value)
+ val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get
result.changes.length shouldBe 2
}
@@ -482,13 +491,14 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR,
+ None,
ttl,
PTRData(Fqdn("ptr"))
)
val input = BatchChangeInput(None, List(ptr), Some(authGrp.id))
- val result = rightResultOf(underTest.applyBatchChange(input, auth, false).value)
+ val result = underTest.applyBatchChange(input, auth, false).value.unsafeRunSync().toOption.get
result.changes.length shouldBe 1
result.changes.head.zoneId shouldBe Some(ipv6PTR17Zone.id)
@@ -513,13 +523,14 @@ class BatchChangeServiceSpec
val ptr = AddChangeInput(
"2001:0000:0000:0001:0000:ff00:0042:8329",
RecordType.PTR,
+ None,
ttl,
PTRData(Fqdn("ptr"))
)
val input = BatchChangeInput(None, List(ptr), Some(authGrp.id))
- val result = rightResultOf(underTest.applyBatchChange(input, auth, false).value)
+ val result = underTest.applyBatchChange(input, auth, false).value.unsafeRunSync().toOption.get
result.changes.length shouldBe 1
result.changes.head.zoneId shouldBe Some(ipv6PTR16Zone.id)
@@ -527,7 +538,7 @@ class BatchChangeServiceSpec
"fail if conversion cannot process" in {
val input = BatchChangeInput(Some("conversionError"), List(apexAddA, nonApexAddA))
- val result = leftResultOf(underTest.applyBatchChange(input, auth, true).value)
+ val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().swap.toOption.get
result shouldBe an[BatchConversionError]
}
@@ -535,7 +546,7 @@ class BatchChangeServiceSpec
"fail with GroupDoesNotExist if owner group ID is provided for a non-existent group" in {
val ownerGroupId = "non-existent-group-id"
val input = BatchChangeInput(None, List(apexAddA), Some(ownerGroupId))
- val result = leftResultOf(underTest.applyBatchChange(input, auth, true).value)
+ val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().swap.toOption.get
result shouldBe InvalidBatchChangeInput(List(GroupDoesNotExist(ownerGroupId)))
}
@@ -543,7 +554,7 @@ class BatchChangeServiceSpec
"fail with UserDoesNotBelongToOwnerGroup if normal user does not belong to group specified by owner group ID" in {
val ownerGroupId = "user-is-not-member"
val input = BatchChangeInput(None, List(apexAddA), Some(ownerGroupId))
- val result = leftResultOf(underTest.applyBatchChange(input, notAuth, true).value)
+ val result = underTest.applyBatchChange(input, notAuth, true).value.unsafeRunSync().swap.toOption.get
result shouldBe
InvalidBatchChangeInput(
@@ -553,7 +564,7 @@ class BatchChangeServiceSpec
"succeed if owner group ID is provided and user is a member of the group" in {
val input = BatchChangeInput(None, List(apexAddA), Some(okGroup.id))
- val result = rightResultOf(underTest.applyBatchChange(input, okAuth, true).value)
+ val result = underTest.applyBatchChange(input, okAuth, true).value.unsafeRunSync().toOption.get
result.changes.length shouldBe 1
}
@@ -562,25 +573,23 @@ class BatchChangeServiceSpec
val ownerGroupId = Some("user-is-not-member")
val input = BatchChangeInput(None, List(apexAddA), ownerGroupId)
val result =
- rightResultOf(
underTest
.applyBatchChange(input, AuthPrincipal(superUser, Seq(baseZone.adminGroupId)), true)
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.changes.length shouldBe 1
}
"succeed with excluded TTL" in {
- val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, AData("1.1.1.1"))
+ val noTtl = AddChangeInput("no-ttl-add.test.com", RecordType.A, None, None, AData("1.1.1.1"))
val withTtl =
- AddChangeInput("with-ttl-add-2.test.com", RecordType.A, Some(900), AData("1.1.1.1"))
- val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT)
+ AddChangeInput("with-ttl-add-2.test.com", RecordType.A, None, Some(900), AData("1.1.1.1"))
+ val noTtlDel = DeleteRRSetChangeInput("non-apex.test.com.", RecordType.TXT, None)
val noTtlUpdate =
- AddChangeInput("non-apex.test.com.", RecordType.TXT, None, TXTData("hello"))
+ AddChangeInput("non-apex.test.com.", RecordType.TXT, None, None, TXTData("hello"))
val input = BatchChangeInput(None, List(noTtl, withTtl, noTtlDel, noTtlUpdate))
- val result = rightResultOf(underTest.applyBatchChange(input, auth, true).value)
+ val result = underTest.applyBatchChange(input, auth, true).value.unsafeRunSync().toOption.get
result.changes.length shouldBe 4
result
@@ -608,15 +617,13 @@ class BatchChangeServiceSpec
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
val result =
- rightResultOf(
underTest
.rejectBatchChange(
batchChange.id,
supportUserAuth,
RejectBatchChangeInput(Some("review comment"))
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.status shouldBe BatchChangeStatus.Rejected
result.approvalStatus shouldBe BatchChangeApprovalStatus.ManuallyRejected
@@ -642,11 +649,9 @@ class BatchChangeServiceSpec
val rejectAuth = AuthPrincipal(supportUser.copy(isTest = true), List())
val result =
- rightResultOf(
underTestManualEnabled
.rejectBatchChange(batchChange.id, rejectAuth, RejectBatchChangeInput(Some("bad")))
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.status shouldBe BatchChangeStatus.Rejected
}
@@ -664,11 +669,9 @@ class BatchChangeServiceSpec
val rejectAuth = AuthPrincipal(supportUser.copy(isTest = true), List())
val result =
- leftResultOf(
underTestManualEnabled
.rejectBatchChange(batchChange.id, rejectAuth, RejectBatchChangeInput(Some("bad")))
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -685,11 +688,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
underTest
.rejectBatchChange(batchChange.id, supportUserAuth, RejectBatchChangeInput())
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe BatchChangeNotPendingReview(batchChange.id)
}
@@ -707,9 +708,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
- underTest.rejectBatchChange(batchChange.id, auth, RejectBatchChangeInput()).value
- )
+ underTest.rejectBatchChange(batchChange.id, auth, RejectBatchChangeInput()).value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -727,9 +726,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
- underTest.rejectBatchChange(batchChange.id, auth, RejectBatchChangeInput()).value
- )
+ underTest.rejectBatchChange(batchChange.id, auth, RejectBatchChangeInput()).value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -748,17 +745,14 @@ class BatchChangeServiceSpec
"succeed if the batchChange is PendingReview and reviewer is authorized" in {
batchChangeRepo.save(batchChangeNeedsApproval)
- val result = {
- rightResultOf(
+ val result =
underTestManualEnabled
.approveBatchChange(
batchChangeNeedsApproval.id,
supportUserAuth,
ApproveBatchChangeInput(Some("reviewed!"))
)
- .value
- )
- }
+ .value.unsafeRunSync().toOption.get
result.userId shouldBe batchChangeNeedsApproval.userId
result.userName shouldBe batchChangeNeedsApproval.userName
@@ -778,15 +772,13 @@ class BatchChangeServiceSpec
val auth = AuthPrincipal(supportUser.copy(isTest = true), List())
val result =
- leftResultOf(
underTestManualEnabled
.approveBatchChange(
batchChangeNeedsApproval.id,
auth,
ApproveBatchChangeInput(Some("reviewed!"))
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChangeNeedsApproval.id)
}
@@ -796,11 +788,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
underTest
.approveBatchChange(batchChange.id, supportUserAuth, ApproveBatchChangeInput())
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe BatchChangeNotPendingReview(batchChange.id)
}
@@ -809,11 +799,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChangeNeedsApproval)
val result =
- leftResultOf(
underTest
.approveBatchChange(batchChangeNeedsApproval.id, auth, ApproveBatchChangeInput())
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChangeNeedsApproval.id)
}
@@ -824,9 +812,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
- underTest.approveBatchChange(batchChange.id, auth, ApproveBatchChangeInput()).value
- )
+ underTest.approveBatchChange(batchChange.id, auth, ApproveBatchChangeInput()).value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -844,11 +830,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
underTest
.approveBatchChange(batchChange.id, superUserAuth, ApproveBatchChangeInput())
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe BatchRequesterNotFound("someOtherUserId", "someUn")
}
@@ -868,11 +852,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- rightResultOf(
underTest
.cancelBatchChange(batchChange.id, auth)
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.status shouldBe BatchChangeStatus.Cancelled
result.approvalStatus shouldBe BatchChangeApprovalStatus.Cancelled
@@ -893,7 +875,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(underTest.cancelBatchChange(batchChange.id, supportUserAuth).value)
+ underTest.cancelBatchChange(batchChange.id, supportUserAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -911,11 +893,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
underTest
.cancelBatchChange(batchChange.id, auth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe BatchChangeNotPendingReview(batchChange.id)
}
@@ -933,11 +913,9 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChange)
val result =
- leftResultOf(
underTest
.cancelBatchChange(batchChange.id, supportUserAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe BatchChangeNotPendingReview(batchChange.id)
}
@@ -956,13 +934,13 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, auth).value)
+ val result = underTest.getBatchChange(batchChange.id, auth).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange)
}
"Fail if batchChange id does not exist" in {
- val result = leftResultOf(underTest.getBatchChange("badId", auth).value)
+ val result = underTest.getBatchChange("badId", auth).value.unsafeRunSync().swap.toOption.get
result shouldBe BatchChangeNotFound("badId")
}
@@ -978,7 +956,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = leftResultOf(underTest.getBatchChange(batchChange.id, notAuth).value)
+ val result = underTest.getBatchChange(batchChange.id, notAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe UserNotAuthorizedError(batchChange.id)
}
@@ -996,7 +974,7 @@ class BatchChangeServiceSpec
val authSuper = notAuth.copy(signedInUser = notAuth.signedInUser.copy(isSuper = true))
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, authSuper).value)
+ val result = underTest.getBatchChange(batchChange.id, authSuper).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange)
}
@@ -1014,7 +992,7 @@ class BatchChangeServiceSpec
val authSuper = notAuth.copy(signedInUser = notAuth.signedInUser.copy(isSupport = true))
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, authSuper).value)
+ val result = underTest.getBatchChange(batchChange.id, authSuper).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange)
}
@@ -1032,7 +1010,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, auth).value)
+ val result = underTest.getBatchChange(batchChange.id, auth).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange, Some(okGroup.name))
}
@@ -1049,7 +1027,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, auth).value)
+ val result = underTest.getBatchChange(batchChange.id, auth).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange)
}
@@ -1069,7 +1047,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.getBatchChange(batchChange.id, auth).value)
+ val result = underTest.getBatchChange(batchChange.id, auth).value.unsafeRunSync().toOption.get
result shouldBe BatchChangeInfo(batchChange, Some(okGroup.name), Some(superUser.userName))
}
}
@@ -1088,7 +1066,7 @@ class BatchChangeServiceSpec
error
)
val zoneMap = ExistingZones(Set(apexZone, baseZone, ptrZone, delegatedPTRZone, ipv6PTRZone))
- val result = await(underTest.getExistingRecordSets(in, zoneMap))
+ val result = underTest.getExistingRecordSets(in, zoneMap).unsafeRunSync()
val expected =
List(existingApex, existingNonApex, existingPtr, existingPtrDelegated, existingPtrV6)
@@ -1105,7 +1083,7 @@ class BatchChangeServiceSpec
error
)
val zoneMap = ExistingZones(Set(apexZone, baseZone, ptrZone, ipv6PTRZone))
- val result = await(underTest.getExistingRecordSets(in, zoneMap))
+ val result = underTest.getExistingRecordSets(in, zoneMap).unsafeRunSync()
val expected =
List(existingApex, existingNonApex, existingPtr, existingPtrV6)
@@ -1115,7 +1093,7 @@ class BatchChangeServiceSpec
"not fail if gets all lefts" in {
val errors = List(error)
val zoneMap = ExistingZones(Set(apexZone, baseZone, ptrZone, delegatedPTRZone, ipv6PTRZone))
- val result = await(underTest.getExistingRecordSets(errors, zoneMap))
+ val result = underTest.getExistingRecordSets(errors, zoneMap).unsafeRunSync()
result.recordSets.length shouldBe 0
}
@@ -1124,42 +1102,42 @@ class BatchChangeServiceSpec
"getZonesForRequest" should {
"return names for the apex and base zones if they both exist" in {
val underTestBaseApexZoneList: ExistingZones =
- await(underTest.getZonesForRequest(List(apexAddA.validNel)))
+ underTest.getZonesForRequest(List(apexAddA.validNel)).unsafeRunSync()
(underTestBaseApexZoneList.zones should contain).allOf(apexZone, baseZone)
}
"return only the apex zone if only the apex zone exists or A or AAAA records" in {
val underTestOnlyApexZoneList: ExistingZones =
- await(underTest.getZonesForRequest(List(onlyApexAddA.validNel)))
+ underTest.getZonesForRequest(List(onlyApexAddA.validNel)).unsafeRunSync()
(underTestOnlyApexZoneList.zones should contain).only(onlyApexZone)
}
"return only the base zone if only the base zone exists" in {
val underTestOnlyBaseZoneList: ExistingZones =
- await(underTest.getZonesForRequest(List(onlyBaseAddAAAA.validNel)))
+ underTest.getZonesForRequest(List(onlyBaseAddAAAA.validNel)).unsafeRunSync()
(underTestOnlyBaseZoneList.zones should contain).only(onlyBaseZone)
}
"return no zones if neither the apex nor base zone exist" in {
val underTestOnlyNoZonesList: ExistingZones =
- await(underTest.getZonesForRequest(List(noZoneAddA.validNel)))
+ underTest.getZonesForRequest(List(noZoneAddA.validNel)).unsafeRunSync()
underTestOnlyNoZonesList.zones shouldBe Set()
}
"return all possible zones for a dotted host" in {
val underTestZonesList: ExistingZones =
- await(underTest.getZonesForRequest(List(dottedAddA.validNel)))
+ underTest.getZonesForRequest(List(dottedAddA.validNel)).unsafeRunSync()
(underTestZonesList.zones should contain).allOf(apexZone, baseZone)
}
"return all possible zones given an IPv4 PTR" in {
val underTestPTRZonesList: ExistingZones =
- await(underTest.getZonesForRequest(List(ptrAdd.validNel)))
+ underTest.getZonesForRequest(List(ptrAdd.validNel)).unsafeRunSync()
(underTestPTRZonesList.zones should contain).allOf(ptrZone, delegatedPTRZone)
}
@@ -1198,8 +1176,8 @@ class BatchChangeServiceSpec
"0.1.0.0.2.ip6.arpa."
)
- val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
- val underTestPTRZonesList: ExistingZones = await(underTest.getZonesForRequest(List(ptr)))
+ val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
+ val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
val zoneNames = underTestPTRZonesList.zones.map(_.name)
zoneNames should contain theSameElementsAs possibleZones
@@ -1224,8 +1202,8 @@ class BatchChangeServiceSpec
)
val ip = "2001:0db8:0000:0000:0000:ff00:0042:8329"
- val ptr = AddChangeInput(ip, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
- val underTestPTRZonesList: ExistingZones = await(underTest.getZonesForRequest(List(ptr)))
+ val ptr = AddChangeInput(ip, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
+ val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(List(ptr)).unsafeRunSync()
val zoneNames = underTestPTRZonesList.zones.map(_.name)
zoneNames shouldBe Set("0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.")
@@ -1271,10 +1249,10 @@ class BatchChangeServiceSpec
val ips = ip1 :: ip2s
val ptrs = ips.map { v6Name =>
- AddChangeInput(v6Name, RecordType.PTR, ttl, PTRData(Fqdn("ptr."))).validNel
+ AddChangeInput(v6Name, RecordType.PTR, None, ttl, PTRData(Fqdn("ptr."))).validNel
}
- val underTestPTRZonesList: ExistingZones = await(underTest.getZonesForRequest(ptrs))
+ val underTestPTRZonesList: ExistingZones = underTest.getZonesForRequest(ptrs).unsafeRunSync()
val zoneNames = underTestPTRZonesList.zones.map(_.name)
zoneNames should contain theSameElementsAs (possibleZones1 ++ possibleZones2)
@@ -1282,7 +1260,7 @@ class BatchChangeServiceSpec
"return a set of distinct zones, given duplicates" in {
val underTestDistinctZonesList: ExistingZones =
- await(underTest.getZonesForRequest(List(cnameReverseAdd.validNel, ptrAdd.validNel)))
+ underTest.getZonesForRequest(List(cnameReverseAdd.validNel, ptrAdd.validNel)).unsafeRunSync()
underTestDistinctZonesList.zones.count(_.id == "nonDelegatedPTR") shouldBe 1
}
@@ -1326,10 +1304,10 @@ class BatchChangeServiceSpec
"properly discover records in forward zones" in {
val apex = apexZone.name
- val aApex = AddChangeInput(apex, RecordType.A, ttl, AData("1.2.3.4"))
- val aNormal = AddChangeInput(s"record.$apex", RecordType.A, ttl, AData("1.2.3.4"))
+ val aApex = AddChangeInput(apex, RecordType.A, None, ttl, AData("1.2.3.4"))
+ val aNormal = AddChangeInput(s"record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
val aDotted =
- AddChangeInput(s"some.dotted.record.$apex", RecordType.A, ttl, AData("1.2.3.4"))
+ AddChangeInput(s"some.dotted.record.$apex", RecordType.A, None, ttl, AData("1.2.3.4"))
val expected = List(
AddChangeForValidation(apexZone, apex, aApex, 7200L),
@@ -1348,10 +1326,10 @@ class BatchChangeServiceSpec
"properly discover TXT records" in {
val apex = apexZone.name
- val txtApex = AddChangeInput(apex, RecordType.TXT, ttl, TXTData("test"))
- val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, ttl, TXTData("test"))
+ val txtApex = AddChangeInput(apex, RecordType.TXT, None, ttl, TXTData("test"))
+ val txtNormal = AddChangeInput(s"record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
val txtDotted =
- AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, ttl, TXTData("test"))
+ AddChangeInput(s"some.dotted.record.$apex", RecordType.TXT, None, ttl, TXTData("test"))
val expected = List(
AddChangeForValidation(apexZone, apex, txtApex, 7200L),
@@ -1445,20 +1423,22 @@ class BatchChangeServiceSpec
val ptrv6ZoneBig = Zone("0.1.0.0.2.ip6.arpa.", "email", id = "ptrv6big")
val smallZoneAdd =
- AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
+ AddChangeInput("2001:db8::ff00:42:8329", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
val medZoneAdd = AddChangeInput(
"2001:0db8:0111:0000:0000:ff00:0042:8329",
RecordType.PTR,
+ None,
ttl,
PTRData(Fqdn("ptr"))
)
val bigZoneAdd = AddChangeInput(
"2001:0000:0000:0000:0000:ff00:0042:8329",
RecordType.PTR,
+ None,
ttl,
PTRData(Fqdn("ptr"))
)
- val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, ttl, PTRData(Fqdn("ptr")))
+ val notFoundZoneAdd = AddChangeInput("::1", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr")))
val ptripv6Adds = List(
smallZoneAdd.validNel,
@@ -1704,7 +1684,7 @@ class BatchChangeServiceSpec
"return a BatchChange if all data inputs are valid/soft failures and manual review is enabled and owner group ID " +
"is provided" in {
- val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
+ val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTestManualEnabled
.buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete), Some("owner-group-ID")),
@@ -1831,7 +1811,7 @@ class BatchChangeServiceSpec
}
"return a BatchChangeErrorList if all data inputs are valid/soft failures and manual review is disabled" in {
- val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
+ val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTest
.buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
@@ -1851,7 +1831,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, scheduled, " +
"and manual review is disabled" in {
- val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
+ val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTest
.buildResponse(
BatchChangeInput(
@@ -1898,7 +1878,7 @@ class BatchChangeServiceSpec
"return a BatchChangeErrorList if all data inputs are valid/soft failures, manual review is enabled, " +
"but batch change allowManualReview attribute is false" in {
- val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT)
+ val delete = DeleteRRSetChangeInput("some.test.delete.", RecordType.TXT, None)
val result = underTestManualEnabled
.buildResponse(
BatchChangeInput(None, List(apexAddA, onlyBaseAddAAAA, delete)),
@@ -2033,7 +2013,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value)
+ val result = underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2072,7 +2052,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChangeTwo)
- val result = rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value)
+ val result = underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2106,7 +2086,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChangeTwo)
- val result = rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 1).value)
+ val result = underTest.listBatchChangeSummaries(auth, maxItems = 1).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 1
result.nextId shouldBe Some(1)
@@ -2139,14 +2119,13 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChangeTwo)
- val result = rightResultOf(
+ val result =
underTest
.listBatchChangeSummaries(
auth,
approvalStatus = Some(BatchChangeApprovalStatus.PendingReview)
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2158,6 +2137,224 @@ class BatchChangeServiceSpec
result.batchChanges(0).createdTimestamp shouldBe batchChangeOne.createdTimestamp
}
+ "return list of batchChangeSummaries filtered by userName if some exist" in {
+ val batchChangeOne =
+ BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.PendingReview
+ )
+ batchChangeRepo.save(batchChangeOne)
+
+ val batchChangeTwo = BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.ofEpochMilli(Instant.now.truncatedTo(ChronoUnit.MILLIS).toEpochMilli + 1000),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved
+ )
+ batchChangeRepo.save(batchChangeTwo)
+
+ val result =
+ underTest
+ .listBatchChangeSummaries(
+ auth,
+ userName = Some(auth.signedInUser.userName)
+ )
+ .value.unsafeRunSync().toOption.get
+
+ result.maxItems shouldBe 100
+ result.nextId shouldBe None
+ result.startFrom shouldBe None
+ result.ignoreAccess shouldBe false
+ result.userName shouldBe Some(auth.signedInUser.userName)
+
+ result.batchChanges.length shouldBe 2
+ result.batchChanges(0).userName shouldBe batchChangeOne.userName
+ result.batchChanges(1).userName shouldBe batchChangeTwo.userName
+ }
+
+ "return list of batchChangeSummaries filtered by date time range if some exist" in {
+
+ val batchChangeOne =
+ BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.PendingReview
+ )
+ batchChangeRepo.save(batchChangeOne)
+
+ // Convert Instant to LocalDateTime in a specific time zone (e.g., UTC)
+ val zoneId: ZoneId = ZoneId.of("UTC")
+ val startDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.minusSeconds(5), zoneId)
+ val endDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.plusSeconds(5), zoneId)
+
+ // Define the desired date-time format
+ val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+
+ // Format LocalDateTime to a string
+ val startDateTimeRange: String = startDateTime.format(formatter)
+ val endDateTimeRange: String = endDateTime.format(formatter)
+
+ val batchChangeTwo = BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.ofEpochMilli(Instant.now.truncatedTo(ChronoUnit.MILLIS).toEpochMilli).plusSeconds(10),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved
+ )
+ batchChangeRepo.save(batchChangeTwo)
+
+ val result =
+ underTest
+ .listBatchChangeSummaries(
+ auth,
+ dateTimeStartRange = Some(startDateTimeRange),
+ dateTimeEndRange = Some(endDateTimeRange)
+ )
+ .value.unsafeRunSync().toOption.get
+
+ result.maxItems shouldBe 100
+ result.nextId shouldBe None
+ result.startFrom shouldBe None
+ result.ignoreAccess shouldBe false
+ result.dateTimeStartRange shouldBe Some(startDateTimeRange)
+ result.dateTimeEndRange shouldBe Some(endDateTimeRange)
+
+ // only get the first batch saved as it is within the date time filter range
+ result.batchChanges.length shouldBe 1
+ result.batchChanges.head.createdTimestamp shouldBe batchChangeOne.createdTimestamp
+ result.batchChanges.head.id shouldBe batchChangeOne.id
+ }
+
+ "return list of batchChangeSummaries filtered by date time range and submitter name if some exist" in {
+
+ val batchChangeOne =
+ BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.PendingReview
+ )
+ batchChangeRepo.save(batchChangeOne)
+
+ // Convert Instant to LocalDateTime in a specific time zone (e.g., UTC)
+ val zoneId: ZoneId = ZoneId.of("UTC")
+ val startDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.minusSeconds(5), zoneId)
+ val endDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.plusSeconds(5), zoneId)
+
+ // Define the desired date-time format
+ val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+
+ // Format LocalDateTime to a string
+ val startDateTimeRange: String = startDateTime.format(formatter)
+ val endDateTimeRange: String = endDateTime.format(formatter)
+
+ val batchChangeTwo = BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.ofEpochMilli(Instant.now.truncatedTo(ChronoUnit.MILLIS).toEpochMilli).plusSeconds(10),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved
+ )
+ batchChangeRepo.save(batchChangeTwo)
+
+ val result =
+ underTest
+ .listBatchChangeSummaries(
+ auth,
+ userName = Some(auth.signedInUser.userName),
+ dateTimeStartRange = Some(startDateTimeRange),
+ dateTimeEndRange = Some(endDateTimeRange)
+ )
+ .value.unsafeRunSync().toOption.get
+
+ result.maxItems shouldBe 100
+ result.nextId shouldBe None
+ result.startFrom shouldBe None
+ result.ignoreAccess shouldBe false
+ result.dateTimeStartRange shouldBe Some(startDateTimeRange)
+ result.dateTimeEndRange shouldBe Some(endDateTimeRange)
+ result.userName shouldBe Some(auth.signedInUser.userName)
+
+ result.batchChanges.length shouldBe 1
+ result.batchChanges.head.createdTimestamp shouldBe batchChangeOne.createdTimestamp
+ result.batchChanges.head.id shouldBe batchChangeOne.id
+ result.batchChanges.head.userName shouldBe batchChangeOne.userName
+ }
+
+ "return list of batchChangeSummaries filtered by date time range, submitter name and approval status if some exist" in {
+
+ val batchChangeOne =
+ BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.PendingReview
+ )
+ batchChangeRepo.save(batchChangeOne)
+
+ // Convert Instant to LocalDateTime in a specific time zone (e.g., UTC)
+ val zoneId: ZoneId = ZoneId.of("UTC")
+ val startDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.minusSeconds(5), zoneId)
+ val endDateTime: LocalDateTime = LocalDateTime.ofInstant(batchChangeOne.createdTimestamp.plusSeconds(5), zoneId)
+
+ // Define the desired date-time format
+ val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+
+ // Format LocalDateTime to a string
+ val startDateTimeRange: String = startDateTime.format(formatter)
+ val endDateTimeRange: String = endDateTime.format(formatter)
+
+ val batchChangeTwo = BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.ofEpochMilli(Instant.now.truncatedTo(ChronoUnit.MILLIS).toEpochMilli).plusSeconds(10),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved
+ )
+ batchChangeRepo.save(batchChangeTwo)
+
+ val result =
+ underTest
+ .listBatchChangeSummaries(
+ auth,
+ userName = Some(auth.signedInUser.userName),
+ dateTimeStartRange = Some(startDateTimeRange),
+ dateTimeEndRange = Some(endDateTimeRange),
+ approvalStatus = Some(BatchChangeApprovalStatus.PendingReview)
+ )
+ .value.unsafeRunSync().toOption.get
+
+ result.maxItems shouldBe 100
+ result.nextId shouldBe None
+ result.startFrom shouldBe None
+ result.ignoreAccess shouldBe false
+ result.dateTimeStartRange shouldBe Some(startDateTimeRange)
+ result.dateTimeEndRange shouldBe Some(endDateTimeRange)
+ result.userName shouldBe Some(auth.signedInUser.userName)
+ result.approvalStatus shouldBe Some(BatchChangeApprovalStatus.PendingReview)
+
+ result.batchChanges.length shouldBe 1
+ result.batchChanges.head.createdTimestamp shouldBe batchChangeOne.createdTimestamp
+ result.batchChanges.head.id shouldBe batchChangeOne.id
+ result.batchChanges.head.userName shouldBe batchChangeOne.userName
+ }
+
"return an offset list of batchChangeSummaries if some exist" in {
val batchChangeOne =
BatchChange(
@@ -2181,7 +2378,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChangeTwo)
val result =
- rightResultOf(underTest.listBatchChangeSummaries(auth, startFrom = Some(1)).value)
+ underTest.listBatchChangeSummaries(auth, startFrom = Some(1)).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2215,7 +2412,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChangeUserTwo)
val result =
- rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value).batchChanges
+ underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get.batchChanges
result.length shouldBe 1
result(0).createdTimestamp shouldBe batchChangeUserOne.createdTimestamp
@@ -2244,7 +2441,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChangeUserTwo)
val result =
- rightResultOf(underTest.listBatchChangeSummaries(auth, ignoreAccess = true).value).batchChanges
+ underTest.listBatchChangeSummaries(auth, ignoreAccess = true).value.unsafeRunSync().toOption.get.batchChanges
result.length shouldBe 1
result(0).createdTimestamp shouldBe batchChangeUserOne.createdTimestamp
@@ -2273,7 +2470,7 @@ class BatchChangeServiceSpec
batchChangeRepo.save(batchChangeUserTwo)
val result =
- rightResultOf(underTest.listBatchChangeSummaries(superUserAuth, ignoreAccess = true).value)
+ underTest.listBatchChangeSummaries(superUserAuth, ignoreAccess = true).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2287,7 +2484,7 @@ class BatchChangeServiceSpec
"return an empty list of batchChangeSummaries if none exist" in {
val result =
- rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value).batchChanges
+ underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get.batchChanges
result.length shouldBe 0
}
@@ -2305,7 +2502,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value)
+ val result = underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2330,7 +2527,7 @@ class BatchChangeServiceSpec
)
batchChangeRepo.save(batchChange)
- val result = rightResultOf(underTest.listBatchChangeSummaries(auth, maxItems = 100).value)
+ val result = underTest.listBatchChangeSummaries(auth, maxItems = 100).value.unsafeRunSync().toOption.get
result.maxItems shouldBe 100
result.nextId shouldBe None
@@ -2341,33 +2538,72 @@ class BatchChangeServiceSpec
result.batchChanges(0).ownerGroupId shouldBe Some("no-existo")
result.batchChanges(0).ownerGroupName shouldBe None
}
+
+ "return list of batchChangeSummaries filtered by batch change status" in {
+ val batchChangeOne =
+ BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.now.truncatedTo(ChronoUnit.MILLIS),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.PendingReview,
+ )
+ batchChangeRepo.save(batchChangeOne)
+
+ val batchChangeTwo = BatchChange(
+ auth.userId,
+ auth.signedInUser.userName,
+ None,
+ Instant.ofEpochMilli(Instant.now.truncatedTo(ChronoUnit.MILLIS).toEpochMilli + 1000),
+ List(),
+ approvalStatus = BatchChangeApprovalStatus.AutoApproved,
+ )
+ batchChangeRepo.save(batchChangeTwo)
+
+ val result =
+ underTest
+ .listBatchChangeSummaries(
+ auth,
+ batchStatus = Some(BatchChangeStatus.PendingReview)
+ )
+ .value.unsafeRunSync().toOption.get
+
+ result.maxItems shouldBe 100
+ result.nextId shouldBe None
+ result.startFrom shouldBe None
+ result.ignoreAccess shouldBe false
+ result.batchStatus shouldBe Some(BatchChangeStatus.PendingReview)
+
+ result.batchChanges.length shouldBe 2
+ }
}
"getOwnerGroup" should {
"return None if owner group ID is None" in {
- rightResultOf(underTest.getOwnerGroup(None).value) shouldBe None
+ underTest.getOwnerGroup(None).value.unsafeRunSync().toOption.get shouldBe None
}
"return None if group does not exist for owner group ID" in {
- rightResultOf(underTest.getOwnerGroup(Some("non-existent-group-id")).value) shouldBe None
+ underTest.getOwnerGroup(Some("non-existent-group-id")).value.unsafeRunSync().toOption.get shouldBe None
}
"return the group if the group exists for the owner group ID" in {
- rightResultOf(underTest.getOwnerGroup(Some(okGroup.id)).value) shouldBe Some(okGroup)
+ underTest.getOwnerGroup(Some(okGroup.id)).value.unsafeRunSync().toOption.get shouldBe Some(okGroup)
}
}
"getReviewer" should {
"return None if reviewer ID is None" in {
- rightResultOf(underTest.getReviewer(None).value) shouldBe None
+ underTest.getReviewer(None).value.unsafeRunSync().toOption.get shouldBe None
}
"return None if reviewer does not exist for the given reviewer ID" in {
- rightResultOf(underTest.getReviewer(Some("non-existent-user-id")).value) shouldBe None
+ underTest.getReviewer(Some("non-existent-user-id")).value.unsafeRunSync().toOption.get shouldBe None
}
"return the reviewer if the reviewer exists for the given reviewer ID" in {
- rightResultOf(underTest.getReviewer(Some(superUser.id)).value) shouldBe Some(superUser)
+ underTest.getReviewer(Some(superUser.id)).value.unsafeRunSync().toOption.get shouldBe Some(superUser)
}
}
@@ -2383,7 +2619,7 @@ class BatchChangeServiceSpec
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
- val result = rightResultOf(
+ val result =
underTestManualEnabled
.convertOrSave(
batchChange,
@@ -2391,8 +2627,8 @@ class BatchChangeServiceSpec
ChangeForValidationMap(List(), ExistingRecordSets(List())),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.reviewComment shouldBe Some("batchSentToConverter")
}
"not send to the converter, save the change if PendingReview and MA enabled" in {
@@ -2406,7 +2642,7 @@ class BatchChangeServiceSpec
approvalStatus = BatchChangeApprovalStatus.PendingReview
)
- val result = rightResultOf(
+ val result =
underTestManualEnabled
.convertOrSave(
batchChange,
@@ -2414,8 +2650,7 @@ class BatchChangeServiceSpec
ChangeForValidationMap(List(), ExistingRecordSets(List())),
None
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
// not sent to converter
result.reviewComment shouldBe None
@@ -2433,7 +2668,7 @@ class BatchChangeServiceSpec
approvalStatus = BatchChangeApprovalStatus.PendingReview
)
- val result = leftResultOf(
+ val result =
underTest
.convertOrSave(
batchChange,
@@ -2441,8 +2676,7 @@ class BatchChangeServiceSpec
ChangeForValidationMap(List(), ExistingRecordSets(List())),
None
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe an[UnknownConversionError]
}
@@ -2457,7 +2691,7 @@ class BatchChangeServiceSpec
approvalStatus = BatchChangeApprovalStatus.ManuallyApproved
)
- val result = leftResultOf(
+ val result =
underTest
.convertOrSave(
batchChange,
@@ -2465,8 +2699,8 @@ class BatchChangeServiceSpec
ChangeForValidationMap(List(), ExistingRecordSets(List())),
None
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
result shouldBe an[UnknownConversionError]
}
}
@@ -2530,7 +2764,7 @@ class BatchChangeServiceSpec
"combine gets for each valid record" in {
val in = List(apexAddForVal.validNel, error)
- val result = rightResultOf(underTest.getGroupIdsFromUnauthorizedErrors(in).value)
+ val result = underTest.getGroupIdsFromUnauthorizedErrors(in).value.unsafeRunSync().toOption.get
result shouldBe Set(okGroup)
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala
index 85960b107..bf940d4b0 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/batch/BatchChangeValidationsSpec.scala
@@ -65,7 +65,8 @@ class BatchChangeValidationsSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.manualReviewConfig,
VinylDNSTestHelpers.batchChangeConfig,
- VinylDNSTestHelpers.scheduledChangesConfig
+ VinylDNSTestHelpers.scheduledChangesConfig,
+ VinylDNSTestHelpers.approvedNameServers
)
import underTest._
@@ -91,12 +92,12 @@ class BatchChangeValidationsSpec
private val validAChangeGen: Gen[AddChangeInput] = for {
fqdn <- domainGenerator
ip <- validIpv4Gen
- } yield AddChangeInput(fqdn, RecordType.A, ttl, AData(ip))
+ } yield AddChangeInput(fqdn, RecordType.A, None, ttl, AData(ip))
private val validAAAAChangeGen: Gen[AddChangeInput] = for {
fqdn <- domainGenerator
ip <- validIpv6Gen
- } yield AddChangeInput(fqdn, RecordType.AAAA, ttl, AAAAData(ip))
+ } yield AddChangeInput(fqdn, RecordType.AAAA, None, ttl, AAAAData(ip))
private val validAddChangeForValidationGen: Gen[AddChangeForValidation] = for {
recordName <- domainComponentGenerator
@@ -106,7 +107,7 @@ class BatchChangeValidationsSpec
private def generateValidAddChangeForValidation(rs: RecordSet): Gen[AddChangeForValidation] =
for {
recordName <- domainComponentGenerator
- addChangeInput <- AddChangeInput(recordName, rs.typ, Some(rs.ttl), rs.records.head)
+ addChangeInput <- AddChangeInput(recordName, rs.typ, None, Some(rs.ttl), rs.records.head)
} yield AddChangeForValidation(validZone, recordName, addChangeInput, defaultTtl)
private val recordSetList = List(rsOk, aaaa, aaaaOrigin, abcRecord)
@@ -120,59 +121,59 @@ class BatchChangeValidationsSpec
private val createPrivateAddChange = AddChangeForValidation(
okZone,
"private-create",
- AddChangeInput("private-create", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("private-create", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
private val createSharedAddChange = AddChangeForValidation(
sharedZone,
"shared-create",
- AddChangeInput("shared-create", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("shared-create", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
private val updatePrivateAddChange = AddChangeForValidation(
okZone,
"private-update",
- AddChangeInput("private-update", RecordType.A, ttl, AAAAData("1.2.3.4")),
+ AddChangeInput("private-update", RecordType.A, None, ttl, AAAAData("1.2.3.4")),
defaultTtl
)
private val updatePrivateDeleteChange = DeleteRRSetChangeForValidation(
okZone,
"private-update",
- DeleteRRSetChangeInput("private-update", RecordType.A)
+ DeleteRRSetChangeInput("private-update", RecordType.A, None)
)
private val updateSharedAddChange = AddChangeForValidation(
sharedZone,
"shared-update",
- AddChangeInput("shared-update", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("shared-update", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
private val updateSharedDeleteChange = DeleteRRSetChangeForValidation(
sharedZone,
"shared-update",
- DeleteRRSetChangeInput("shared-update", RecordType.AAAA)
+ DeleteRRSetChangeInput("shared-update", RecordType.AAAA, None)
)
private val deleteSingleRecordChange = DeleteRRSetChangeForValidation(
sharedZone,
"shared-update",
- DeleteRRSetChangeInput("shared-update", RecordType.AAAA, Some(AAAAData("1:0::1")))
+ DeleteRRSetChangeInput("shared-update", RecordType.AAAA, None, Some(AAAAData("1:0::1")))
)
private val deletePrivateChange = DeleteRRSetChangeForValidation(
okZone,
"private-delete",
- DeleteRRSetChangeInput("private-delete", RecordType.A)
+ DeleteRRSetChangeInput("private-delete", RecordType.A, None)
)
private val deleteSharedChange = DeleteRRSetChangeForValidation(
sharedZone,
"shared-delete",
- DeleteRRSetChangeInput("shared-delete", RecordType.AAAA)
+ DeleteRRSetChangeInput("shared-delete", RecordType.AAAA, None)
)
private val validPendingBatchChange = BatchChange(
@@ -200,7 +201,7 @@ class BatchChangeValidationsSpec
AddChangeForValidation(
okZone,
s"$recordName",
- AddChangeInput(s"$recordName.ok.", RecordType.A, ttl, aData),
+ AddChangeInput(s"$recordName.ok.", RecordType.A, None, ttl, aData),
defaultTtl
)
@@ -211,7 +212,7 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
okZone,
s"$recordName",
- DeleteRRSetChangeInput(s"$recordName.ok.", RecordType.A, recordData)
+ DeleteRRSetChangeInput(s"$recordName.ok.", RecordType.A, None, recordData)
)
property("validateBatchChangeInputSize: should fail if batch has no changes") {
@@ -233,6 +234,61 @@ class BatchChangeValidationsSpec
}
}
+ property(
+ "isApprovedNameServer: should be valid if the name server is on approved name server list"
+ ) {
+ isApprovedNameServer(VinylDNSTestHelpers.approvedNameServers, NSData(Fqdn("some.test.ns."))) shouldBe ().validNel
+ }
+
+ property(
+ "isApprovedNameServer: should throw an error if the name server is not on approved name server list"
+ ) {
+ val nsData = NSData(Fqdn("not.valid."))
+ isApprovedNameServer(VinylDNSTestHelpers.approvedNameServers, nsData) shouldBe NotApprovedNSError(nsData.nsdname.fqdn).invalidNel
+ }
+
+ property(
+ "containsApprovedNameServers: should be valid if the name server is on approved name server list"
+ ) {
+ containsApprovedNameServers(NSData(Fqdn("some.test.ns.")), VinylDNSTestHelpers.approvedNameServers) shouldBe ().validNel
+ }
+
+ property(
+ "containsApprovedNameServers: should throw an error if the name server is not on approved name server list"
+ ) {
+ val nsData = NSData(Fqdn("not.valid."))
+ containsApprovedNameServers(nsData, VinylDNSTestHelpers.approvedNameServers) shouldBe NotApprovedNSError(nsData.nsdname.fqdn).invalidNel
+ }
+
+ property(
+ "isOriginRecord: should return true if the record is origin"
+ ) {
+ isOriginRecord("@", "ok.") shouldBe true
+ isOriginRecord("ok.", "ok.") shouldBe true
+ }
+
+ property(
+ "isOriginRecord: should return false if the record is not origin"
+ ) {
+ isOriginRecord("dummy.ok.", "ok.") shouldBe false
+ }
+
+ property(
+ "isNotOrigin: should be valid if the record is not origin"
+ ) {
+ val recordSetName = "ok.zone.recordsets."
+ val error = s"Record with name $recordSetName is an NS record at apex and cannot be added"
+ isNotOrigin(recordSetName, okZone, error) shouldBe InvalidBatchRequest(error).invalidNel
+ }
+
+ property(
+ "isNotOrigin: should throw an error if the record is origin"
+ ) {
+ val recordSetName = "test."
+ val error = s"Record with name $recordSetName is an NS record at apex and cannot be added"
+ isNotOrigin(recordSetName, okZone, error) shouldBe ().validNel
+ }
+
property(
"validateScheduledChange: should fail if batch is scheduled and scheduled change disabled"
) {
@@ -348,7 +404,7 @@ class BatchChangeValidationsSpec
) {
val input = BatchChangeInput(
None,
- List(AddChangeInput("private-create", RecordType.A, ttl, AData("1.1.1.1"))),
+ List(AddChangeInput("private-create", RecordType.A, None, ttl, AData("1.1.1.1"))),
scheduledTime = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS))
)
val bcv =
@@ -357,7 +413,8 @@ class BatchChangeValidationsSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.manualReviewConfig,
VinylDNSTestHelpers.batchChangeConfig,
- ScheduledChangesConfig(enabled = false)
+ ScheduledChangesConfig(enabled = false),
+ VinylDNSTestHelpers.approvedNameServers
)
bcv.validateBatchChangeInput(input, None, okAuth).value.unsafeRunSync() shouldBe Left(
ScheduledChangesDisabled
@@ -369,7 +426,7 @@ class BatchChangeValidationsSpec
) {
val input = BatchChangeInput(
None,
- List(AddChangeInput("private-create", RecordType.A, ttl, AData("1.1.1.1"))),
+ List(AddChangeInput("private-create", RecordType.A, None, ttl, AData("1.1.1.1"))),
scheduledTime = Some(Instant.now.truncatedTo(ChronoUnit.MILLIS).minus(1, ChronoUnit.HOURS))
)
val bcv =
@@ -378,7 +435,8 @@ class BatchChangeValidationsSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.manualReviewConfig,
VinylDNSTestHelpers.batchChangeConfig,
- ScheduledChangesConfig(enabled = true)
+ ScheduledChangesConfig(enabled = true),
+ VinylDNSTestHelpers.approvedNameServers
)
bcv.validateBatchChangeInput(input, None, okAuth).value.unsafeRunSync() shouldBe Left(
ScheduledTimeMustBeInFuture
@@ -507,30 +565,46 @@ class BatchChangeValidationsSpec
}
property("validateInputChanges: should fail with mix of success and failure inputs") {
- val goodInput = AddChangeInput("test.example.com.", RecordType.A, ttl, AData("1.1.1.1"))
+ val goodNSInput = AddChangeInput("test-ns.example.com.", RecordType.NS, None, ttl, NSData(Fqdn("some.test.ns.")))
+ val goodNAPTRInput = AddChangeInput("test-naptr.example.com.", RecordType.NAPTR, None, ttl, NAPTRData(1, 2, "S", "E2U+sip", "!^.*$!sip:jd@corpxyz.com!", Fqdn("target")))
+ val goodSRVInput = AddChangeInput("test-srv.example.com.", RecordType.SRV, None, ttl, SRVData(1, 2, 3, Fqdn("target.vinyldns.")))
+ val badNSInput = AddChangeInput("test-bad-ns.example.com.", RecordType.NS, None, ttl, NSData(Fqdn("some.te$st.ns.")))
+ val badNAPTRInput = AddChangeInput("test-bad-naptr.example.com.", RecordType.NAPTR, None, ttl, NAPTRData(99999, 2, "S", "E2U+sip", "", Fqdn("target")))
+ val badNAPTRFlagInput = AddChangeInput("test-bad-flag-naptr.example.com.", RecordType.NAPTR, None, ttl, NAPTRData(1, 2, "t", "E2U+sip", "", Fqdn("target")))
+ val badNAPTRRegexpInput = AddChangeInput("test-bad-regexp-naptr.example.com.", RecordType.NAPTR, None, ttl, NAPTRData(1, 2, "S", "E2U+sip", "dummyregexp", Fqdn("target")))
+ val badSRVInput = AddChangeInput("test-bad-srv.example.com.", RecordType.SRV, None, ttl, SRVData(99999, 2, 3, Fqdn("target.vinyldns.")))
+ val goodInput = AddChangeInput("test.example.com.", RecordType.A, None, ttl, AData("1.1.1.1"))
val goodAAAAInput =
- AddChangeInput("testAAAA.example.com.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput("testAAAA.example.com.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
val invalidDomainNameInput =
- AddChangeInput("invalidDomainName$", RecordType.A, ttl, AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput("invalidDomainName$", RecordType.A, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
val invalidIpv6Input =
- AddChangeInput("testbad.example.com.", RecordType.AAAA, ttl, AAAAData("invalidIpv6:123"))
+ AddChangeInput("testbad.example.com.", RecordType.AAAA, None, ttl, AAAAData("invalidIpv6:123"))
val result =
validateInputChanges(
- List(goodInput, goodAAAAInput, invalidDomainNameInput, invalidIpv6Input),
+ List(goodNSInput, goodNAPTRInput, goodSRVInput, goodInput, goodAAAAInput, invalidDomainNameInput, invalidIpv6Input, badNSInput, badNAPTRInput, badNAPTRFlagInput, badNAPTRRegexpInput, badSRVInput),
false
)
result(0) shouldBe valid
result(1) shouldBe valid
- result(2) should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
- result(3) should haveInvalid[DomainValidationError](InvalidIpv6Address("invalidIpv6:123"))
+ result(2) shouldBe valid
+ result(3) shouldBe valid
+ result(4) shouldBe valid
+ result(5) should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
+ result(6) should haveInvalid[DomainValidationError](InvalidIpv6Address("invalidIpv6:123"))
+ result(7) should haveInvalid[DomainValidationError](InvalidDomainName("some.te$st.ns."))
+ result(8) should haveInvalid[DomainValidationError](InvalidMX_NAPTR_SRVData(99999, 0, 65535, "order", "NAPTR"))
+ result(9) should haveInvalid[DomainValidationError](InvalidNaptrFlag("t"))
+ result(10) should haveInvalid[DomainValidationError](InvalidNaptrRegexp("dummyregexp"))
+ result(11) should haveInvalid[DomainValidationError](InvalidMX_NAPTR_SRVData(99999, 0, 65535, "priority", "SRV"))
}
property("""validateInputName: should fail with a HighValueDomainError
- |if inputName is a High Value Domain""".stripMargin) {
- val changeA = AddChangeInput("high-value-domain.foo.", RecordType.A, ttl, AData("1.1.1.1"))
- val changeIpV4 = AddChangeInput("192.0.2.252", RecordType.PTR, ttl, PTRData(Fqdn("test.")))
+ |if inputName is a High Value Domain""".stripMargin) {
+ val changeA = AddChangeInput("high-value-domain.foo.", RecordType.A, None, ttl, AData("1.1.1.1"))
+ val changeIpV4 = AddChangeInput("192.0.2.252", RecordType.PTR, None, ttl, PTRData(Fqdn("test.")))
val changeIpV6 =
- AddChangeInput("fd69:27cc:fe91:0:0:0:0:ffff", RecordType.PTR, ttl, PTRData(Fqdn("test.")))
+ AddChangeInput("fd69:27cc:fe91:0:0:0:0:ffff", RecordType.PTR, None, ttl, PTRData(Fqdn("test.")))
val resultA = validateInputName(changeA, false)
val resultIpV4 = validateInputName(changeIpV4, false)
@@ -547,10 +621,10 @@ class BatchChangeValidationsSpec
property("""validateInputName: should fail with a RecordRequiresManualReview
|if inputName is matches domain requiring manual review""".stripMargin) {
- val changeA = AddChangeInput("needs-review.foo.", RecordType.A, ttl, AData("1.1.1.1"))
- val changeIpV4 = AddChangeInput("192.0.2.254", RecordType.PTR, ttl, PTRData(Fqdn("test.")))
+ val changeA = AddChangeInput("needs-review.foo.", RecordType.A, None, ttl, AData("1.1.1.1"))
+ val changeIpV4 = AddChangeInput("192.0.2.254", RecordType.PTR, None, ttl, PTRData(Fqdn("test.")))
val changeIpV6 =
- AddChangeInput("fd69:27cc:fe91:0:0:0:ffff:1", RecordType.PTR, ttl, PTRData(Fqdn("test.")))
+ AddChangeInput("fd69:27cc:fe91:0:0:0:ffff:1", RecordType.PTR, None, ttl, PTRData(Fqdn("test.")))
val resultA = validateInputName(changeA, false)
val resultIpV4 = validateInputName(changeIpV4, false)
@@ -566,14 +640,14 @@ class BatchChangeValidationsSpec
}
property("doesNotRequireManualReview: should succeed if user is reviewing") {
- val changeA = AddChangeInput("needs-review.foo.", RecordType.A, ttl, AData("1.1.1.1"))
+ val changeA = AddChangeInput("needs-review.foo.", RecordType.A, None, ttl, AData("1.1.1.1"))
validateInputName(changeA, true) should beValid(())
}
property("""zoneDoesNotRequireManualReview: should fail with RecordRequiresManualReview
- |if zone name matches domain requiring manual review""".stripMargin) {
+ |if zone name matches domain requiring manual review""".stripMargin) {
val addChangeInput =
- AddChangeInput("not-allowed.zone.NEEDS.review", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("not-allowed.zone.NEEDS.review", RecordType.A, None, ttl, AData("1.1.1.1"))
val addChangeForValidation = AddChangeForValidation(
Zone("Zone.needs.review", "some@email.com"),
"not-allowed",
@@ -588,7 +662,7 @@ class BatchChangeValidationsSpec
property("""zoneDoesNotRequireManualReview: should succeed if user is reviewing""") {
val addChangeInput =
- AddChangeInput("not-allowed.zone.NEEDS.review", RecordType.A, ttl, AData("1.1.1.1"))
+ AddChangeInput("not-allowed.zone.NEEDS.review", RecordType.A, None, ttl, AData("1.1.1.1"))
val addChangeForValidation = AddChangeForValidation(
Zone("Zone.needs.review", "some@email.com"),
"not-allowed",
@@ -599,25 +673,25 @@ class BatchChangeValidationsSpec
}
property("""validateInputName: should fail with a DomainValidationError for deletes
- |if validateHostName fails for an invalid domain name""".stripMargin) {
- val change = DeleteRRSetChangeInput("invalidDomainName$", RecordType.A)
+ |if validateHostName fails for an invalid domain name""".stripMargin) {
+ val change = DeleteRRSetChangeInput("invalidDomainName$", RecordType.A, None)
val result = validateInputName(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
}
property("""validateInputName: should fail with a DomainValidationError for deletes
- |if validateHostName fails for an invalid domain name length""".stripMargin) {
+ |if validateHostName fails for an invalid domain name length""".stripMargin) {
val invalidDomainName = Random.alphanumeric.take(256).mkString
- val change = DeleteRRSetChangeInput(invalidDomainName, RecordType.AAAA)
+ val change = DeleteRRSetChangeInput(invalidDomainName, RecordType.AAAA, None)
val result = validateInputName(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName."))
.and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255)))
}
property("""validateInputName: PTR should fail with InvalidIPAddress for deletes
- |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
+ |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
val invalidIp = "invalidIp.111"
- val change = DeleteRRSetChangeInput(invalidIp, RecordType.PTR)
+ val change = DeleteRRSetChangeInput(invalidIp, RecordType.PTR, None)
val result = validateInputName(change, false)
result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp))
}
@@ -639,16 +713,16 @@ class BatchChangeValidationsSpec
}
property("""validateAddChangeInput: should fail with a DomainValidationError
- |if validateHostName fails for an invalid domain name""".stripMargin) {
- val change = AddChangeInput("invalidDomainName$", RecordType.A, ttl, AData("1.1.1.1"))
+ |if validateHostName fails for an invalid domain name""".stripMargin) {
+ val change = AddChangeInput("invalidDomainName$", RecordType.A, None, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("invalidDomainName$."))
}
property("""validateAddChangeInput: should fail with a DomainValidationError
- |if validateHostName fails for an invalid domain name length""".stripMargin) {
+ |if validateHostName fails for an invalid domain name length""".stripMargin) {
val invalidDomainName = Random.alphanumeric.take(256).mkString
- val change = AddChangeInput(invalidDomainName, RecordType.A, ttl, AData("1.1.1.1"))
+ val change = AddChangeInput(invalidDomainName, RecordType.A, None, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidDomainName."))
.and(haveInvalid[DomainValidationError](InvalidLength(s"$invalidDomainName.", 2, 255)))
@@ -659,7 +733,7 @@ class BatchChangeValidationsSpec
) {
forAll(choose[Long](0, 29)) { invalidTTL: Long =>
val change =
- AddChangeInput("test.comcast.com.", RecordType.A, Some(invalidTTL), AData("1.1.1.1"))
+ AddChangeInput("test.comcast.com.", RecordType.A, None, Some(invalidTTL), AData("1.1.1.1"))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](
InvalidTTL(invalidTTL, DomainValidations.TTL_MIN_LENGTH, DomainValidations.TTL_MAX_LENGTH)
@@ -670,22 +744,22 @@ class BatchChangeValidationsSpec
property("""validateAddChangeInput: should fail with InvalidIpv4Address
|if validateRecordData fails for an invalid ipv4 address""".stripMargin) {
val invalidIpv4 = "invalidIpv4:123"
- val change = AddChangeInput("test.comcast.com.", RecordType.A, ttl, AData(invalidIpv4))
+ val change = AddChangeInput("test.comcast.com.", RecordType.A, None, ttl, AData(invalidIpv4))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIpv4Address(invalidIpv4))
}
property("""validateAddChangeInput: should fail with InvalidIpv6Address
- |if validateRecordData fails for an invalid ipv6 address""".stripMargin) {
+ |if validateRecordData fails for an invalid ipv6 address""".stripMargin) {
val invalidIpv6 = "invalidIpv6:123"
- val change = AddChangeInput("test.comcast.com.", RecordType.AAAA, ttl, AAAAData(invalidIpv6))
+ val change = AddChangeInput("test.comcast.com.", RecordType.AAAA, None, ttl, AAAAData(invalidIpv6))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIpv6Address(invalidIpv6))
}
property("validateAddChangeInput: should fail if A inputName includes a reverse zone address") {
val invalidInputName = "test.1.2.3.in-addr.arpa."
- val badAChange = AddChangeInput(invalidInputName, RecordType.A, ttl, AData("1.1.1.1"))
+ val badAChange = AddChangeInput(invalidInputName, RecordType.A, None, ttl, AData("1.1.1.1"))
val result = validateAddChangeInput(badAChange, false)
result should haveInvalid[DomainValidationError](
RecordInReverseZoneError(invalidInputName, RecordType.A.toString)
@@ -695,7 +769,7 @@ class BatchChangeValidationsSpec
property("validateAddChangeInput: should fail if AAAA inputName includes a reverse zone address") {
val invalidInputName = "test.1.2.3.ip6.arpa."
val badAAAAChange =
- AddChangeInput(invalidInputName, RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput(invalidInputName, RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
val result = validateAddChangeInput(badAAAAChange, false)
result should haveInvalid[DomainValidationError](
RecordInReverseZoneError(invalidInputName, RecordType.AAAA.toString)
@@ -703,12 +777,13 @@ class BatchChangeValidationsSpec
}
property("""validateAddChangeInput: should fail with InvalidDomainName
- |if validateRecordData fails for invalid CNAME record data""".stripMargin) {
+ |if validateRecordData fails for invalid CNAME record data""".stripMargin) {
val invalidCNAMERecordData = "$$$"
val change =
AddChangeInput(
"test.comcast.com.",
RecordType.CNAME,
+ None,
ttl,
CNAMEData(Fqdn(invalidCNAMERecordData))
)
@@ -717,13 +792,30 @@ class BatchChangeValidationsSpec
result should haveInvalid[DomainValidationError](InvalidCname(s"$invalidCNAMERecordData.",false))
}
+ property("""validateAddChangeInput: should fail with Invalid CNAME
+ |if validateRecordData fails for IPv4 Address in CNAME record data""".stripMargin) {
+ val invalidCNAMERecordData = "1.2.3.4"
+ val change =
+ AddChangeInput(
+ "test.comcast.com.",
+ RecordType.CNAME,
+ None,
+ ttl,
+ CNAMEData(Fqdn(invalidCNAMERecordData))
+ )
+ val result = validateAddChangeInput(change, false)
+
+ result should haveInvalid[DomainValidationError](InvalidIPv4CName(s"Fqdn($invalidCNAMERecordData.)"))
+ }
+
property("""validateAddChangeInput: should fail with InvalidLength
- |if validateRecordData fails for invalid CNAME record data""".stripMargin) {
+ |if validateRecordData fails for invalid CNAME record data""".stripMargin) {
val invalidCNAMERecordData = "s" * 256
val change =
AddChangeInput(
"test.comcast.com.",
RecordType.CNAME,
+ None,
ttl,
CNAMEData(Fqdn(invalidCNAMERecordData))
)
@@ -735,9 +827,9 @@ class BatchChangeValidationsSpec
}
property("""validateAddChangeInput: PTR should fail with InvalidIPAddress
- |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
+ |if inputName is not a valid ipv4 or ipv6 address""".stripMargin) {
val invalidIp = "invalidip.111."
- val change = AddChangeInput(invalidIp, RecordType.PTR, ttl, PTRData(Fqdn("test.comcast.com")))
+ val change = AddChangeInput(invalidIp, RecordType.PTR, None, ttl, PTRData(Fqdn("test.comcast.com")))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidIPAddress(invalidIp))
@@ -745,7 +837,7 @@ class BatchChangeValidationsSpec
property("validateAddChangeInput: should fail with InvalidDomainName for invalid PTR record data") {
val invalidPTRDname = "*invalidptrdname"
- val change = AddChangeInput("4.5.6.7", RecordType.PTR, ttl, PTRData(Fqdn(invalidPTRDname)))
+ val change = AddChangeInput("4.5.6.7", RecordType.PTR, None, ttl, PTRData(Fqdn(invalidPTRDname)))
val result = validateAddChangeInput(change, false)
result should haveInvalid[DomainValidationError](InvalidDomainName(s"$invalidPTRDname."))
@@ -756,28 +848,46 @@ class BatchChangeValidationsSpec
) {
val authZone = okZone
val reverseZone = okZone.copy(name = "2.0.192.in-addr.arpa.")
+ val addNsRecord = AddChangeForValidation(
+ okZone,
+ "ns-add",
+ AddChangeInput("ns-add.ok.", RecordType.NS, None, ttl, NSData(Fqdn("some.test.ns."))),
+ defaultTtl
+ )
+ val addNaptrRecord = AddChangeForValidation(
+ okZone,
+ "naptr-add",
+ AddChangeInput("naptr-add.ok.", RecordType.NAPTR, None, ttl, NAPTRData(1, 2, "S", "E2U+sip", "", Fqdn("target"))),
+ defaultTtl
+ )
+ val addSrvRecord = AddChangeForValidation(
+ okZone,
+ "srv-add",
+ AddChangeInput("srv-add.ok.", RecordType.SRV, None, ttl, SRVData(1, 2, 3, Fqdn("target.vinyldns."))),
+ defaultTtl
+ )
val addA1 = AddChangeForValidation(
authZone,
"valid",
- AddChangeInput("valid.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("valid.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val existingA = AddChangeForValidation(
authZone,
"existingA",
- AddChangeInput("existingA.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("existingA.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val existingCname = AddChangeForValidation(
authZone,
"existingCname",
- AddChangeInput("existingCname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname"))),
+ AddChangeInput("existingCname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname"))),
defaultTtl
)
val addA2 = AddChangeForValidation(
okZone,
"valid2",
- AddChangeInput("valid2.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("valid2.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val duplicateNameCname = AddChangeForValidation(
@@ -786,6 +896,7 @@ class BatchChangeValidationsSpec
AddChangeInput(
"199.2.0.192.in-addr.arpa.",
RecordType.CNAME,
+ None,
ttl,
CNAMEData(Fqdn("199.192/30.2.0.192.in-addr.arpa"))
),
@@ -794,7 +905,7 @@ class BatchChangeValidationsSpec
val duplicateNamePTR = AddChangeForValidation(
reverseZone,
"199",
- AddChangeInput("192.0.2.199", RecordType.PTR, ttl, PTRData(Fqdn("ptr.ok."))),
+ AddChangeInput("192.0.2.199", RecordType.PTR, None, ttl, PTRData(Fqdn("ptr.ok."))),
defaultTtl
)
@@ -810,6 +921,9 @@ class BatchChangeValidationsSpec
val result = validateChangesWithContext(
ChangeForValidationMap(
List(
+ addNsRecord.validNel,
+ addNaptrRecord.validNel,
+ addSrvRecord.validNel,
addA1.validNel,
existingA.validNel,
existingCname.validNel,
@@ -825,21 +939,24 @@ class BatchChangeValidationsSpec
)
result(0) shouldBe valid
- result(1) should haveInvalid[DomainValidationError](
- RecordAlreadyExists(existingA.inputChange.inputName, existingA.inputChange.record, false)
+ result(1) shouldBe valid
+ result(2) shouldBe valid
+ result(3) shouldBe valid
+ result(4) should haveInvalid[DomainValidationError](
+ RecordAlreadyExists(existingA.inputChange.inputName)
)
- result(2) should haveInvalid[DomainValidationError](
- RecordAlreadyExists(existingCname.inputChange.inputName, existingCname.inputChange.record, false)
+ result(5) should haveInvalid[DomainValidationError](
+ RecordAlreadyExists(existingCname.inputChange.inputName)
).and(
haveInvalid[DomainValidationError](
CnameIsNotUniqueError(existingCname.inputChange.inputName, existingCname.inputChange.typ)
)
)
- result(3) shouldBe valid
- result(4) should haveInvalid[DomainValidationError](
+ result(6) shouldBe valid
+ result(7) should haveInvalid[DomainValidationError](
RecordNameNotUniqueInBatch("199.2.0.192.in-addr.arpa.", RecordType.CNAME)
)
- result(5) shouldBe valid
+ result(8) shouldBe valid
}
property("validateChangesWithContext: should succeed for valid update inputs") {
@@ -938,13 +1055,13 @@ class BatchChangeValidationsSpec
val addUpdateA = AddChangeForValidation(
okZone.addACLRule(writeAcl),
"update",
- AddChangeInput("update.ok.", RecordType.A, ttl, AData("1.2.3.4")),
+ AddChangeInput("update.ok.", RecordType.A, None, ttl, AData("1.2.3.4")),
defaultTtl
)
val deleteUpdateA = DeleteRRSetChangeForValidation(
okZone.addACLRule(writeAcl),
"update",
- DeleteRRSetChangeInput("update.ok.", RecordType.A)
+ DeleteRRSetChangeInput("update.ok.", RecordType.A, None)
)
val result = validateChangesWithContext(
ChangeForValidationMap(
@@ -969,13 +1086,13 @@ class BatchChangeValidationsSpec
val addUpdateA = AddChangeForValidation(
okZone.addACLRule(readAcl),
"update",
- AddChangeInput("update.ok.", RecordType.A, ttl, AData("1.2.3.4")),
+ AddChangeInput("update.ok.", RecordType.A, None, ttl, AData("1.2.3.4")),
defaultTtl
)
val deleteUpdateA = DeleteRRSetChangeForValidation(
okZone.addACLRule(readAcl),
"update",
- DeleteRRSetChangeInput("update.ok.", RecordType.A)
+ DeleteRRSetChangeInput("update.ok.", RecordType.A, None)
)
val result = validateChangesWithContext(
ChangeForValidationMap(
@@ -1006,6 +1123,29 @@ class BatchChangeValidationsSpec
)
}
+ property("validateChangesWithContext: should fail for update if same record data is provided for add and delete") {
+ val deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", Some(AData("1.2.3.4")))
+ val result = validateChangesWithContext(
+ ChangeForValidationMap(
+ List(
+ makeAddUpdateRecord("deleteRecord"), // Record does not exist
+ deleteRecord
+ ).map(_.validNel),
+ ExistingRecordSets(List(rsOk))
+ ),
+ okAuth,
+ false,
+ None
+ )
+
+ result(0) should haveInvalid[DomainValidationError](
+ InvalidUpdateRequest(makeAddUpdateRecord("deleteRecord").inputChange.inputName)
+ )
+ result(1) should haveInvalid[DomainValidationError](
+ InvalidUpdateRequest(deleteRecord.inputChange.inputName)
+ )
+ }
+
property("validateChangesWithContext: should complete for update if record does not exist") {
val deleteRRSet = makeDeleteUpdateDeleteRRSet("deleteRRSet")
val deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", Some(AData("1.1.1.1")))
@@ -1031,11 +1171,7 @@ class BatchChangeValidationsSpec
result(1) shouldBe valid
result(3) shouldBe valid
result(4) shouldBe valid
- deleteNonExistentEntry.inputChange.record.foreach { record =>
- result(5) should haveInvalid[DomainValidationError](
- DeleteRecordDataDoesNotExist(deleteNonExistentEntry.inputChange.inputName, record)
- )
- }
+ result(5) shouldBe valid
}
property(
@@ -1051,14 +1187,14 @@ class BatchChangeValidationsSpec
val addUpdateA = AddChangeForValidation(
sharedZone,
"mx",
- AddChangeInput("mx.shared.", RecordType.MX, ttl, MXData(200, Fqdn("mx"))),
+ AddChangeInput("mx.shared.", RecordType.MX, None, ttl, MXData(200, Fqdn("mx"))),
defaultTtl
)
val deleteUpdateA =
DeleteRRSetChangeForValidation(
sharedZone,
"mx",
- DeleteRRSetChangeInput("mx.shared.", RecordType.MX)
+ DeleteRRSetChangeInput("mx.shared.", RecordType.MX, None)
)
val result = validateChangesWithContext(
ChangeForValidationMap(
@@ -1082,7 +1218,7 @@ class BatchChangeValidationsSpec
val deleteCnameRRSet = DeleteRRSetChangeForValidation(
okZone,
"deleteRRSet",
- DeleteRRSetChangeInput("deleteRRSet.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("deleteRRSet.ok.", RecordType.CNAME, None)
)
val deleteCnameEntry = DeleteRRSetChangeForValidation(
okZone,
@@ -1090,6 +1226,7 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeInput(
"deleteRecord.ok.",
RecordType.CNAME,
+ None,
Some(CNAMEData(Fqdn("cname.data.")))
)
)
@@ -1184,7 +1321,7 @@ class BatchChangeValidationsSpec
)
result(0) should haveInvalid[DomainValidationError](
- RecordAlreadyExists(input.inputChange.inputName, input.inputChange.record, false)
+ RecordAlreadyExists(input.inputChange.inputName)
)
}
}
@@ -1195,13 +1332,13 @@ class BatchChangeValidationsSpec
val addCname = AddChangeForValidation(
validZone,
"existingCname",
- AddChangeInput("existingCname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname"))),
+ AddChangeInput("existingCname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname"))),
defaultTtl
)
val deleteA = DeleteRRSetChangeForValidation(
validZone,
"existingCname",
- DeleteRRSetChangeInput("existingCname.ok.", RecordType.A)
+ DeleteRRSetChangeInput("existingCname.ok.", RecordType.A, None)
)
val existingA = rsOk.copy(zoneId = addCname.zone.id, name = addCname.recordName)
val newRecordSetList = existingA :: recordSetList
@@ -1224,7 +1361,7 @@ class BatchChangeValidationsSpec
val addCname = AddChangeForValidation(
validZone,
"existingCname",
- AddChangeInput("existingCname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname"))),
+ AddChangeInput("existingCname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname"))),
defaultTtl
)
val existingA = rsOk.copy(zoneId = addCname.zone.id, name = addCname.recordName)
@@ -1246,13 +1383,13 @@ class BatchChangeValidationsSpec
val addCname = AddChangeForValidation(
validIp4ReverseZone,
"30",
- AddChangeInput("30.2.0.192.in-addr.arpa.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname"))),
+ AddChangeInput("30.2.0.192.in-addr.arpa.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname"))),
defaultTtl
)
val deletePtr = DeleteRRSetChangeForValidation(
validIp4ReverseZone,
"30",
- DeleteRRSetChangeInput("192.0.2.30", RecordType.PTR)
+ DeleteRRSetChangeInput("192.0.2.30", RecordType.PTR, None)
)
val ptr4 = ptrIp4.copy(zoneId = validIp4ReverseZone.id)
val result = validateChangesWithContext(
@@ -1277,6 +1414,7 @@ class BatchChangeValidationsSpec
AddChangeInput(
"0.6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.9.e.f.c.c.7.2.9.6.d.f.ip6.arpa.",
RecordType.CNAME,
+ None,
ttl,
CNAMEData(Fqdn("cname"))
),
@@ -1300,19 +1438,19 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addAAAA = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("testAAAA.ok.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
val addCname = AddChangeForValidation(
okZone,
"new",
- AddChangeInput("new.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey.ok.com."))),
+ AddChangeInput("new.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey.ok.com."))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1335,19 +1473,19 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addDuplicateCname = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey.ok.com."))),
+ AddChangeInput("testAAAA.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey.ok.com."))),
defaultTtl
)
val addAAAA = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("testAAAA.ok.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1375,19 +1513,19 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addCname = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey.ok.com."))),
+ AddChangeInput("testAAAA.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey.ok.com."))),
defaultTtl
)
val addDuplicateCname = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey2.ok.com."))),
+ AddChangeInput("testAAAA.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey2.ok.com."))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1417,19 +1555,19 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addPtr = AddChangeForValidation(
okZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("test.ok."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("test.ok."))),
defaultTtl
)
val addDuplicatePtr = AddChangeForValidation(
okZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("hey.ok.com."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("hey.ok.com."))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1450,7 +1588,7 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
validZone,
"valid",
- AddChangeInput("valid.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("valid.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val result =
@@ -1470,7 +1608,7 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
validZone,
"valid",
- AddChangeInput("valid.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("valid.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1496,7 +1634,7 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
validZone.addACLRule(ACLRule(accessLevel = AccessLevel.Write, userId = Some(notAuth.userId))),
"valid",
- AddChangeInput("valid.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("valid.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val result =
@@ -1539,13 +1677,13 @@ class BatchChangeValidationsSpec
val addCname = AddChangeForValidation(
validZone,
"existing",
- AddChangeInput("existing.ok.", RecordType.CNAME, ttl, PTRData(Fqdn("orders.vinyldns."))),
+ AddChangeInput("existing.ok.", RecordType.CNAME, None, ttl, PTRData(Fqdn("orders.vinyldns."))),
defaultTtl
)
val addPtr = AddChangeForValidation(
validZone,
"existing",
- AddChangeInput("existing.ok.", RecordType.PTR, ttl, CNAMEData(Fqdn("ptrdname."))),
+ AddChangeInput("existing.ok.", RecordType.PTR, None, ttl, CNAMEData(Fqdn("ptrdname."))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1566,7 +1704,7 @@ class BatchChangeValidationsSpec
val deleteA = DeleteRRSetChangeForValidation(
validZone,
"Record-exists",
- DeleteRRSetChangeInput("record-exists.ok.", RecordType.A)
+ DeleteRRSetChangeInput("record-exists.ok.", RecordType.A, None)
)
val existingDeleteRecord =
rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName.toLowerCase)
@@ -1610,7 +1748,7 @@ class BatchChangeValidationsSpec
val deleteA = DeleteRRSetChangeForValidation(
validZone,
"Active-record-status",
- DeleteRRSetChangeInput("active-record-status", RecordType.A)
+ DeleteRRSetChangeInput("active-record-status", RecordType.A, None)
)
val existingDeleteRecord = rsOk.copy(
zoneId = deleteA.zone.id,
@@ -1636,7 +1774,7 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
validZone,
"valid",
- DeleteRRSetChangeInput("valid.ok.", RecordType.A)
+ DeleteRRSetChangeInput("valid.ok.", RecordType.A, None)
)
val existingDeleteRecord = rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName)
val result = validateChangesWithContext(
@@ -1658,7 +1796,7 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
validZone,
"valid",
- DeleteRRSetChangeInput("valid.ok.", RecordType.A)
+ DeleteRRSetChangeInput("valid.ok.", RecordType.A, None)
)
val existingDeleteRecord = rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName)
val result = validateChangesWithContext(
@@ -1689,7 +1827,7 @@ class BatchChangeValidationsSpec
ACLRule(accessLevel = AccessLevel.Delete, userId = Some(notAuth.userId))
),
"valid",
- DeleteRRSetChangeInput("valid.ok.", RecordType.A)
+ DeleteRRSetChangeInput("valid.ok.", RecordType.A, None)
)
val existingDeleteRecord = rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName)
val result = validateChangesWithContext(
@@ -1712,7 +1850,7 @@ class BatchChangeValidationsSpec
val deleteA = DeleteRRSetChangeForValidation(
validZone.addACLRule(ACLRule(accessLevel = AccessLevel.Write, userId = Some(notAuth.userId))),
"valid",
- DeleteRRSetChangeInput("valid.ok.", RecordType.A)
+ DeleteRRSetChangeInput("valid.ok.", RecordType.A, None)
)
val existingDeleteRecord = rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName)
val result = validateChangesWithContext(
@@ -1740,13 +1878,13 @@ class BatchChangeValidationsSpec
val addDuplicateA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.com.", RecordType.A, ttl, AData("10.1.1.1")),
+ AddChangeInput("test.com.", RecordType.A, None, ttl, AData("10.1.1.1")),
defaultTtl
)
val addDuplicateCname = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.com.", RecordType.CNAME, ttl, CNAMEData(Fqdn("thing.com."))),
+ AddChangeInput("test.com.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("thing.com."))),
defaultTtl
)
@@ -1754,24 +1892,24 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
okZone,
"delete",
- DeleteRRSetChangeInput("delete.ok.", RecordType.A)
+ DeleteRRSetChangeInput("delete.ok.", RecordType.A, None)
)
val addCname = AddChangeForValidation(
okZone,
"delete",
- AddChangeInput("delete.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("thing.com."))),
+ AddChangeInput("delete.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("thing.com."))),
defaultTtl
)
val addA = AddChangeForValidation(
okZone,
"delete-this",
- AddChangeInput("delete-this.ok.", RecordType.A, ttl, AData("10.1.1.1")),
+ AddChangeInput("delete-this.ok.", RecordType.A, None, ttl, AData("10.1.1.1")),
defaultTtl
)
val deleteCname = DeleteRRSetChangeForValidation(
okZone,
"delete",
- DeleteRRSetChangeInput("delete-this.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("delete-this.ok.", RecordType.CNAME, None)
)
val existingA = rsOk.copy(zoneId = deleteA.zone.id, name = deleteA.recordName)
val existingCname =
@@ -1807,29 +1945,29 @@ class BatchChangeValidationsSpec
val existingA = rsOk.copy(name = "new")
val deleteA =
- DeleteRRSetChangeForValidation(okZone, "new", DeleteRRSetChangeInput("new.ok.", RecordType.A))
+ DeleteRRSetChangeForValidation(okZone, "new", DeleteRRSetChangeInput("new.ok.", RecordType.A, None))
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addAAAA = AddChangeForValidation(
okZone,
"testAAAA",
- AddChangeInput("testAAAA.ok.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("testAAAA.ok.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
val addCname = AddChangeForValidation(
okZone,
"new",
- AddChangeInput("new.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey.ok."))),
+ AddChangeInput("new.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey.ok."))),
defaultTtl
)
val addPtr = AddChangeForValidation(
okZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("test.ok."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("test.ok."))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1854,24 +1992,24 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
okZone,
"new",
- DeleteRRSetChangeInput("new.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("new.ok.", RecordType.CNAME, None)
)
val addA = AddChangeForValidation(
okZone,
"test",
- AddChangeInput("test.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("test.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addAAAA = AddChangeForValidation(
okZone,
"new",
- AddChangeInput("new.ok.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("new.ok.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
val addPtr = AddChangeForValidation(
okZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("test.ok."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("test.ok."))),
defaultTtl
)
@@ -1897,12 +2035,12 @@ class BatchChangeValidationsSpec
DeleteRRSetChangeForValidation(
okZone,
"new",
- DeleteRRSetChangeInput("new.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("new.ok.", RecordType.CNAME, None)
)
val addCname = AddChangeForValidation(
okZone,
"new",
- AddChangeInput("new.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("updateData.com"))),
+ AddChangeInput("new.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("updateData.com"))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -1928,18 +2066,18 @@ class BatchChangeValidationsSpec
val deleteUpdateCname = DeleteRRSetChangeForValidation(
okZone,
"name-conflict",
- DeleteRRSetChangeInput("existing.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("existing.ok.", RecordType.CNAME, None)
)
val addUpdateCname = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("add.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("updated.cname."))),
+ AddChangeInput("add.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("updated.cname."))),
defaultTtl
)
val addCname = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("add.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("new.add.cname."))),
+ AddChangeInput("add.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("new.add.cname."))),
defaultTtl
)
@@ -1973,12 +2111,12 @@ class BatchChangeValidationsSpec
val deletePtr = DeleteRRSetChangeForValidation(
validIp4ReverseZone,
"193",
- DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR)
+ DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR, None)
)
val addCname = AddChangeForValidation(
validIp4ReverseZone,
"193",
- AddChangeInput("test.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("hey2.there."))),
+ AddChangeInput("test.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("hey2.there."))),
defaultTtl
)
@@ -2007,12 +2145,12 @@ class BatchChangeValidationsSpec
val deletePtr = DeleteRRSetChangeForValidation(
validIp4ReverseZone,
"193",
- DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR)
+ DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR, None)
)
val addPtr = AddChangeForValidation(
validIp4ReverseZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("updateData.com"))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("updateData.com"))),
defaultTtl
)
val result = validateChangesWithContext(
@@ -2038,18 +2176,18 @@ class BatchChangeValidationsSpec
val deleteUpdatePtr = DeleteRRSetChangeForValidation(
validIp4ReverseZone,
"193",
- DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR)
+ DeleteRRSetChangeInput("192.0.2.193", RecordType.PTR, None)
)
val addUpdatePtr = AddChangeForValidation(
validIp4ReverseZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("updated.ptr."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("updated.ptr."))),
defaultTtl
)
val addPtr = AddChangeForValidation(
validIp4ReverseZone,
"193",
- AddChangeInput("192.0.2.193", RecordType.PTR, ttl, PTRData(Fqdn("new.add.ptr."))),
+ AddChangeInput("192.0.2.193", RecordType.PTR, None, ttl, PTRData(Fqdn("new.add.ptr."))),
defaultTtl
)
@@ -2067,13 +2205,13 @@ class BatchChangeValidationsSpec
}
property("validateAddChangeInput: should succeed for a valid TXT addChangeInput") {
- val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData("test"))
+ val input = AddChangeInput("txt.ok.", RecordType.TXT, None, ttl, TXTData("test"))
val result = validateAddChangeInput(input, false)
result shouldBe valid
}
property("validateAddChangeInput: should fail for a TXT addChangeInput with empty TXTData") {
- val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData(""))
+ val input = AddChangeInput("txt.ok.", RecordType.TXT, None, ttl, TXTData(""))
val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidLength("", 1, 64764))
}
@@ -2082,41 +2220,45 @@ class BatchChangeValidationsSpec
"validateAddChangeInput: should fail for a TXT addChangeInput with TXTData that is too many characters"
) {
val txtData = "x" * 64765
- val input = AddChangeInput("txt.ok.", RecordType.TXT, ttl, TXTData(txtData))
+ val input = AddChangeInput("txt.ok.", RecordType.TXT, None, ttl, TXTData(txtData))
val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidLength(txtData, 1, 64764))
}
property("validateAddChangeInput: should succeed for a valid MX addChangeInput") {
- val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, Fqdn("foo.bar.")))
+ val input = AddChangeInput("mx.ok.", RecordType.MX, None, ttl, MXData(1, Fqdn("foo.bar.")))
val result = validateAddChangeInput(input, false)
result shouldBe valid
}
property("validateAddChangeInput: should fail for a MX addChangeInput with invalid preference") {
- val inputSmall = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, Fqdn("foo.bar.")))
- val inputLarge = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1000000, Fqdn("foo.bar.")))
+ val inputSmall = AddChangeInput("mx.ok.", RecordType.MX, None, ttl, MXData(-1, Fqdn("foo.bar.")))
+ val inputLarge = AddChangeInput("mx.ok.", RecordType.MX, None, ttl, MXData(1000000, Fqdn("foo.bar.")))
val resultSmall = validateAddChangeInput(inputSmall, false)
val resultLarge = validateAddChangeInput(inputLarge, false)
resultSmall should haveInvalid[DomainValidationError](
- InvalidMxPreference(
+ InvalidMX_NAPTR_SRVData(
-1,
- DomainValidations.MX_PREFERENCE_MIN_VALUE,
- DomainValidations.MX_PREFERENCE_MAX_VALUE
+ DomainValidations.INTEGER_MIN_VALUE,
+ DomainValidations.INTEGER_MAX_VALUE,
+ "preference",
+ "MX"
)
)
resultLarge should haveInvalid[DomainValidationError](
- InvalidMxPreference(
+ InvalidMX_NAPTR_SRVData(
1000000,
- DomainValidations.MX_PREFERENCE_MIN_VALUE,
- DomainValidations.MX_PREFERENCE_MAX_VALUE
+ DomainValidations.INTEGER_MIN_VALUE,
+ DomainValidations.INTEGER_MAX_VALUE,
+ "preference",
+ "MX"
)
)
}
property("validateAddChangeInput: should fail for a MX addChangeInput with invalid exchange") {
- val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(1, Fqdn("foo$.bar.")))
+ val input = AddChangeInput("mx.ok.", RecordType.MX, None, ttl, MXData(1, Fqdn("foo$.bar.")))
val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidDomainName("foo$.bar."))
}
@@ -2124,13 +2266,15 @@ class BatchChangeValidationsSpec
property(
"validateAddChangeInput: should fail for a MX addChangeInput with invalid preference and exchange"
) {
- val input = AddChangeInput("mx.ok.", RecordType.MX, ttl, MXData(-1, Fqdn("foo$.bar.")))
+ val input = AddChangeInput("mx.ok.", RecordType.MX, None, ttl, MXData(-1, Fqdn("foo$.bar.")))
val result = validateAddChangeInput(input, false)
result should haveInvalid[DomainValidationError](
- InvalidMxPreference(
+ InvalidMX_NAPTR_SRVData(
-1,
- DomainValidations.MX_PREFERENCE_MIN_VALUE,
- DomainValidations.MX_PREFERENCE_MAX_VALUE
+ DomainValidations.INTEGER_MIN_VALUE,
+ DomainValidations.INTEGER_MAX_VALUE,
+ "preference",
+ "MX"
)
)
result should haveInvalid[DomainValidationError](InvalidDomainName("foo$.bar."))
@@ -2144,13 +2288,13 @@ class BatchChangeValidationsSpec
}
property("validateDeleteChangeInput: should succeed for valid data when record data is passed in") {
- val input = DeleteRRSetChangeInput("a.ok.", RecordType.A, Some(AData("1.1.1.1")))
+ val input = DeleteRRSetChangeInput("a.ok.", RecordType.A, None, Some(AData("1.1.1.1")))
validateDeleteRRSetChangeInput(input, false) shouldBe valid
}
property("validateDeleteChangeInput: should fail when invalid record data is passed in") {
val invalidIp = "invalid IP address"
- val input = DeleteRRSetChangeInput("a.ok.", RecordType.A, Some(AData(invalidIp)))
+ val input = DeleteRRSetChangeInput("a.ok.", RecordType.A, None, Some(AData(invalidIp)))
val result = validateDeleteRRSetChangeInput(input, false)
result should haveInvalid[DomainValidationError](InvalidIpv4Address(invalidIp))
}
@@ -2165,7 +2309,7 @@ class BatchChangeValidationsSpec
val addMX = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("name-conflict", RecordType.MX, ttl, MXData(1, Fqdn("foo.bar."))),
+ AddChangeInput("name-conflict", RecordType.MX, None, ttl, MXData(1, Fqdn("foo.bar."))),
defaultTtl
)
@@ -2183,13 +2327,13 @@ class BatchChangeValidationsSpec
val addMx = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("name-conflict", RecordType.MX, ttl, MXData(1, Fqdn("foo.bar."))),
+ AddChangeInput("name-conflict", RecordType.MX, None, ttl, MXData(1, Fqdn("foo.bar."))),
defaultTtl
)
val addMx2 = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("name-conflict", RecordType.MX, ttl, MXData(2, Fqdn("foo.bar."))),
+ AddChangeInput("name-conflict", RecordType.MX, None, ttl, MXData(2, Fqdn("foo.bar."))),
defaultTtl
)
@@ -2212,12 +2356,12 @@ class BatchChangeValidationsSpec
val deleteMx = DeleteRRSetChangeForValidation(
okZone,
"name-conflict",
- DeleteRRSetChangeInput("name-conflict", RecordType.MX)
+ DeleteRRSetChangeInput("name-conflict", RecordType.MX, None)
)
val addMx = AddChangeForValidation(
okZone,
"name-conflict",
- AddChangeInput("name-conflict", RecordType.MX, ttl, MXData(1, Fqdn("foo.bar."))),
+ AddChangeInput("name-conflict", RecordType.MX, None, ttl, MXData(1, Fqdn("foo.bar."))),
defaultTtl
)
@@ -2407,16 +2551,16 @@ class BatchChangeValidationsSpec
val update1 = updateSharedAddChange.copy(
inputChange =
- AddChangeInput("shared-update.shared", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8"))
+ AddChangeInput("shared-update.shared", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8"))
)
val update2 = updateSharedAddChange.copy(
- inputChange = AddChangeInput("shared-update.shared", RecordType.AAAA, ttl, AAAAData("1::1"))
+ inputChange = AddChangeInput("shared-update.shared", RecordType.AAAA, None, ttl, AAAAData("1::1"))
)
val add1 = createSharedAddChange.copy(
- inputChange = AddChangeInput("shared-add.shared", RecordType.A, ttl, AData("1.2.3.4"))
+ inputChange = AddChangeInput("shared-add.shared", RecordType.A, None, ttl, AData("1.2.3.4"))
)
val add2 = createSharedAddChange.copy(
- inputChange = AddChangeInput("shared-add.shared", RecordType.A, ttl, AData("5.6.7.8"))
+ inputChange = AddChangeInput("shared-add.shared", RecordType.A, None, ttl, AData("5.6.7.8"))
)
val result = underTest.validateChangesWithContext(
@@ -2452,35 +2596,35 @@ class BatchChangeValidationsSpec
val addA = AddChangeForValidation(
okZone,
"dotted.a",
- AddChangeInput("dotted.a.ok.", RecordType.A, ttl, AData("1.1.1.1")),
+ AddChangeInput("dotted.a.ok.", RecordType.A, None, ttl, AData("1.1.1.1")),
defaultTtl
)
val addAAAA = AddChangeForValidation(
okZone,
"dotted.aaaa",
- AddChangeInput("dotted.aaaa.ok.", RecordType.AAAA, ttl, AAAAData("1:2:3:4:5:6:7:8")),
+ AddChangeInput("dotted.aaaa.ok.", RecordType.AAAA, None, ttl, AAAAData("1:2:3:4:5:6:7:8")),
defaultTtl
)
val addCNAME = AddChangeForValidation(
okZone,
"dotted.cname",
- AddChangeInput("dotted.cname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("foo.com"))),
+ AddChangeInput("dotted.cname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("foo.com"))),
defaultTtl
)
val addMX = AddChangeForValidation(
okZone,
"dotted.mx",
- AddChangeInput("dotted.mx.ok.", RecordType.MX, ttl, MXData(1, Fqdn("foo.bar."))),
+ AddChangeInput("dotted.mx.ok.", RecordType.MX, None, ttl, MXData(1, Fqdn("foo.bar."))),
defaultTtl
)
val addTXT = AddChangeForValidation(
okZone,
"dotted.txt",
- AddChangeInput("dotted.txt.ok.", RecordType.TXT, ttl, TXTData("test")),
+ AddChangeInput("dotted.txt.ok.", RecordType.TXT, None, ttl, TXTData("test")),
defaultTtl
)
@@ -2511,27 +2655,27 @@ class BatchChangeValidationsSpec
val deleteA = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.a",
- DeleteRRSetChangeInput("existing.dotted.a.ok.", RecordType.A)
+ DeleteRRSetChangeInput("existing.dotted.a.ok.", RecordType.A, None)
)
val deleteAAAA = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.aaaa",
- DeleteRRSetChangeInput("existing.dotted.aaaa.ok.", RecordType.AAAA)
+ DeleteRRSetChangeInput("existing.dotted.aaaa.ok.", RecordType.AAAA, None)
)
val deleteCname = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.cname",
- DeleteRRSetChangeInput("existing.dotted.cname.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("existing.dotted.cname.ok.", RecordType.CNAME, None)
)
val deleteMX = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.mx",
- DeleteRRSetChangeInput("existing.dotted.mx.ok.", RecordType.MX)
+ DeleteRRSetChangeInput("existing.dotted.mx.ok.", RecordType.MX, None)
)
val deleteTXT = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.txt",
- DeleteRRSetChangeInput("existing.dotted.txt.ok.", RecordType.TXT)
+ DeleteRRSetChangeInput("existing.dotted.txt.ok.", RecordType.TXT, None)
)
val result = validateChangesWithContext(
ChangeForValidationMap(
@@ -2566,33 +2710,33 @@ class BatchChangeValidationsSpec
val deleteA = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.a",
- DeleteRRSetChangeInput("existing.dotted.a.ok.", RecordType.A)
+ DeleteRRSetChangeInput("existing.dotted.a.ok.", RecordType.A, None)
)
val deleteAAAA = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.aaaa",
- DeleteRRSetChangeInput("existing.dotted.aaaa.ok.", RecordType.AAAA)
+ DeleteRRSetChangeInput("existing.dotted.aaaa.ok.", RecordType.AAAA, None)
)
val deleteCname = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.cname",
- DeleteRRSetChangeInput("existing.dotted.cname.ok.", RecordType.CNAME)
+ DeleteRRSetChangeInput("existing.dotted.cname.ok.", RecordType.CNAME, None)
)
val deleteMX = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.mx",
- DeleteRRSetChangeInput("existing.dotted.mx.ok.", RecordType.MX)
+ DeleteRRSetChangeInput("existing.dotted.mx.ok.", RecordType.MX, None)
)
val deleteTXT = DeleteRRSetChangeForValidation(
okZone,
"existing.dotted.txt",
- DeleteRRSetChangeInput("existing.dotted.txt.ok.", RecordType.TXT)
+ DeleteRRSetChangeInput("existing.dotted.txt.ok.", RecordType.TXT, None)
)
val addUpdateA = AddChangeForValidation(
okZone,
"existing.dotted.a",
- AddChangeInput("existing.dotted.a.ok.", RecordType.A, ttl, AData("1.2.3.4")),
+ AddChangeInput("existing.dotted.a.ok.", RecordType.A, None, ttl, AData("1.2.3.4")),
defaultTtl
)
val addUpdateAAAA = AddChangeForValidation(
@@ -2601,6 +2745,7 @@ class BatchChangeValidationsSpec
AddChangeInput(
"existing.dotted.aaaa.ok.",
RecordType.AAAA,
+ None,
Some(700),
AAAAData("1:2:3:4:5:6:7:8")
),
@@ -2612,6 +2757,7 @@ class BatchChangeValidationsSpec
AddChangeInput(
"existing.dotted.cname.ok.",
RecordType.CNAME,
+ None,
Some(700),
CNAMEData(Fqdn("test"))
),
@@ -2620,13 +2766,13 @@ class BatchChangeValidationsSpec
val addUpdateMX = AddChangeForValidation(
okZone,
"existing.dotted.mx",
- AddChangeInput("existing.dotted.mx.ok.", RecordType.MX, Some(700), MXData(3, Fqdn("mx"))),
+ AddChangeInput("existing.dotted.mx.ok.", RecordType.MX, None, Some(700), MXData(3, Fqdn("mx"))),
defaultTtl
)
val addUpdateTXT = AddChangeForValidation(
okZone,
"existing.dotted.txt",
- AddChangeInput("existing.dotted.txt.ok.", RecordType.TXT, Some(700), TXTData("testing")),
+ AddChangeInput("existing.dotted.txt.ok.", RecordType.TXT, None, Some(700), TXTData("testing")),
defaultTtl
)
@@ -2659,17 +2805,17 @@ class BatchChangeValidationsSpec
}
property("validateAddChangeInput: should fail for a CNAME addChangeInput with forward slash for forward zone") {
- val cnameWithForwardSlash = AddChangeInput("cname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname/")))
+ val cnameWithForwardSlash = AddChangeInput("cname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname/")))
val result = validateAddChangeInput(cnameWithForwardSlash, false)
result should haveInvalid[DomainValidationError](InvalidCname("cname/.",false))
}
property("validateAddChangeInput: should succeed for a valid CNAME addChangeInput without forward slash for forward zone") {
- val cname = AddChangeInput("cname.ok.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname")))
+ val cname = AddChangeInput("cname.ok.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname")))
val result = validateAddChangeInput(cname, false)
result shouldBe valid
}
property("validateAddChangeInput: should succeed for a valid CNAME addChangeInput with forward slash for reverse zone") {
- val cnameWithForwardSlash = AddChangeInput("2.0.192.in-addr.arpa.", RecordType.CNAME, ttl, CNAMEData(Fqdn("cname/")))
+ val cnameWithForwardSlash = AddChangeInput("2.0.192.in-addr.arpa.", RecordType.CNAME, None, ttl, CNAMEData(Fqdn("cname/")))
val result = validateAddChangeInput(cnameWithForwardSlash, true)
result shouldBe valid
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
index 923f89959..569c11616 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipServiceSpec.scala
@@ -25,11 +25,11 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import vinyldns.api.Interfaces._
-import vinyldns.api.ResultHelpers
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.zone.ZoneRepository
import cats.effect._
import scalikejdbc.{ConnectionPool, DB}
+import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData._
@@ -41,7 +41,6 @@ class MembershipServiceSpec
with Matchers
with MockitoSugar
with BeforeAndAfterEach
- with ResultHelpers
with EitherMatchers {
private val mockGroupRepo = mock[GroupRepository]
@@ -50,6 +49,8 @@ class MembershipServiceSpec
private val mockZoneRepo = mock[ZoneRepository]
private val mockGroupChangeRepo = mock[GroupChangeRepository]
private val mockRecordSetRepo = mock[RecordSetRepository]
+ private val mockValidEmailConfig = ValidEmailConfig(valid_domains = List("test.com","*dummy.com"),2)
+ private val mockValidEmailConfigNew = ValidEmailConfig(valid_domains = List(),2)
private val backingService = new MembershipService(
mockGroupRepo,
@@ -57,9 +58,20 @@ class MembershipServiceSpec
mockMembershipRepo,
mockZoneRepo,
mockGroupChangeRepo,
- mockRecordSetRepo
+ mockRecordSetRepo,
+ mockValidEmailConfig
+ )
+ private val backingServiceNew = new MembershipService(
+ mockGroupRepo,
+ mockUserRepo,
+ mockMembershipRepo,
+ mockZoneRepo,
+ mockGroupChangeRepo,
+ mockRecordSetRepo,
+ mockValidEmailConfigNew
)
private val underTest = spy(backingService)
+ private val underTestNew = spy(backingServiceNew)
private val okUserInfo: UserInfo = UserInfo(okUser)
private val dummyUserInfo: UserInfo = UserInfo(dummyUser)
@@ -84,7 +96,7 @@ class MembershipServiceSpec
// the update will remove users 3 and 4, add users 5 and 6, as well as a new admin user 7 and remove user2 as admin
private val updatedInfo = Group(
name = "new.name",
- email = "new.email",
+ email = "test@test.com",
description = Some("new desc"),
id = "id",
memberIds = Set("user1", "user2", "user5", "user6", "user7"),
@@ -123,7 +135,7 @@ class MembershipServiceSpec
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val result: Group = rightResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val result: Group = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().toOption.get
result shouldBe groupInfo
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
@@ -151,7 +163,7 @@ class MembershipServiceSpec
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val result: Group = rightResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val result: Group = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().toOption.get
result shouldBe groupInfo
val groupChangeCaptor = ArgumentCaptor.forClass(classOf[GroupChange])
@@ -180,7 +192,7 @@ class MembershipServiceSpec
).thenReturn(IO.pure(expectedMembersAdded))
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val result: Group = rightResultOf(underTest.createGroup(info, okAuth).value)
+ val result: Group = underTest.createGroup(info, okAuth).value.unsafeRunSync().toOption.get
result shouldBe info
val memberIdCaptor = ArgumentCaptor.forClass(classOf[Set[String]])
@@ -207,7 +219,7 @@ class MembershipServiceSpec
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val result: Group = rightResultOf(underTest.createGroup(info, okAuth).value)
+ val result: Group = underTest.createGroup(info, okAuth).value.unsafeRunSync().toOption.get
result.memberIds should contain(okAuth.userId)
}
@@ -217,7 +229,7 @@ class MembershipServiceSpec
.when(underTest)
.groupWithSameNameDoesNotExist(groupInfo.name)
- val error = leftResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val error = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[GroupAlreadyExistsError]
verify(mockGroupRepo, never()).save(any[DB], any[Group])
@@ -233,7 +245,7 @@ class MembershipServiceSpec
.when(underTest)
.usersExist(groupInfo.memberIds)
- val error = leftResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val error = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[UserNotFoundError]
verify(mockGroupRepo, never()).save(any[DB], any[Group])
@@ -249,7 +261,7 @@ class MembershipServiceSpec
doReturn(IO.raiseError(new RuntimeException("fail"))).when(mockGroupRepo).save(any[DB], any[Group])
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val error = leftResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val error = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[RuntimeException]
verify(mockMembershipRepo, never())
@@ -267,7 +279,7 @@ class MembershipServiceSpec
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
- val error = leftResultOf(underTest.createGroup(groupInfo, okAuth).value)
+ val error = underTest.createGroup(groupInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[RuntimeException]
}
@@ -277,15 +289,179 @@ class MembershipServiceSpec
.when(underTest)
.groupValidation(groupInfo.copy(name = "", email = ""))
- val error = leftResultOf(underTest.createGroup(groupInfo.copy(name = "", email = ""), okAuth).value)
+ val error = underTest.createGroup(groupInfo.copy(name = "", email = ""), okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[GroupValidationError]
verify(mockGroupRepo, never()).save(any[DB], any[Group])
verify(mockMembershipRepo, never())
.saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
}
+
+ "return an error if an invalid domain is entered" in {
+ val error = underTest.createGroup(groupInfo.copy(email = "test@ok.com"), okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an invalid email is entered" in {
+ val error = underTest.createGroup(groupInfo.copy(email = "test.ok.com"), okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an invalid email with number of dots is entered" in {
+ val error = underTest.createGroup(groupInfo.copy(email = "test@ok.ok.dummy.com"), okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an invalid email with * is entered" in {
+ val error = underTest.createGroup(groupInfo.copy(email = "test@*dummy.com"), okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
}
+ "return an error if an email is invalid test case 1" in {
+ val error = underTest.emailValidation(email = "test.ok.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if a domain is invalid test case 1" in {
+ val error = underTest.emailValidation(email = "test@ok.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 2" in {
+ val error = underTest.emailValidation(email = "test@.@.test.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 3" in {
+ val error = underTest.emailValidation(email = "test@.@@.test.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 4" in {
+ val error = underTest.emailValidation(email = "@te@st@test.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 5" in {
+ val error = underTest.emailValidation(email = ".test@test.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 6" in {
+ val error = underTest.emailValidation(email = "te.....st@test.com").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 7" in {
+ val error = underTest.emailValidation(email = "test@test.com.").value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "Check whether *dummy.com is a valid email" in {
+ val result = underTest.emailValidation(email = "test@ok.dummy.com").value.unsafeRunSync()
+ result shouldBe Right(())
+ }
+
+ "Check whether test.com is a valid email" in {
+ val result = underTest.emailValidation(email = "test@test.com").value.unsafeRunSync()
+ result shouldBe Right(())
+ }
+
+ "Check whether test.com is a valid email with number of dots" in {
+ val result = underTest.emailValidation(email = "test@ok.dummy.com").value.unsafeRunSync()
+ result shouldBe Right(())
+ }
+
+ "Check whether it is allowing any domain when the config is empty" in {
+ val result = underTestNew.emailValidation(email = "test@abc.com").value.unsafeRunSync()
+ result shouldBe Right(())
+ }
+
+ "Create Group when email has domain *dummy.com" in {
+ doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok")
+ doReturn(().toResult).when(underTest).groupValidation(groupInfo)
+ doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name)
+ doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds)
+ doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group])
+ doReturn(IO.pure(Set(okUser.id)))
+ .when(mockMembershipRepo)
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
+
+ val result = underTest.createGroup(groupInfo.copy(email = "test@ok.dummy.com"), okAuth).value.unsafeRunSync().toOption.get
+ result shouldBe groupInfo.copy(email = "test@ok.dummy.com")
+
+ val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
+
+ verify(mockMembershipRepo, times(2))
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
+
+ val savedGroup = groupCaptor.getValue
+ (savedGroup.memberIds should contain).only(okUser.id)
+ (savedGroup.adminUserIds should contain).only(okUser.id)
+ savedGroup.name shouldBe groupInfo.name
+ savedGroup.email shouldBe groupInfo.copy(email = "test@ok.dummy.com").email
+ savedGroup.description shouldBe groupInfo.description
+ }
+
+ "Create Group when email with any domain when config is empty" in {
+ doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok")
+ doReturn(().toResult).when(underTestNew).groupValidation(groupInfo)
+ doReturn(().toResult).when(underTestNew).groupWithSameNameDoesNotExist(groupInfo.name)
+ doReturn(().toResult).when(underTestNew).usersExist(groupInfo.memberIds)
+ doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group])
+ doReturn(IO.pure(Set(okUser.id)))
+ .when(mockMembershipRepo)
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
+
+ val result = underTestNew.createGroup(groupInfo.copy(email = "test@abc.com"), okAuth).value.unsafeRunSync().toOption.get
+ result shouldBe groupInfo.copy(email = "test@abc.com")
+
+ val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
+
+ verify(mockMembershipRepo, times(2))
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
+
+ val savedGroup = groupCaptor.getValue
+ (savedGroup.memberIds should contain).only(okUser.id)
+ (savedGroup.adminUserIds should contain).only(okUser.id)
+ savedGroup.name shouldBe groupInfo.name
+ savedGroup.email shouldBe groupInfo.copy(email = "test@abc.com").email
+ savedGroup.description shouldBe groupInfo.description
+ }
+
+ "Create Group when email has domain test.com" in {
+ doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUser("ok")
+ doReturn(().toResult).when(underTest).groupValidation(groupInfo)
+ doReturn(().toResult).when(underTest).groupWithSameNameDoesNotExist(groupInfo.name)
+ doReturn(().toResult).when(underTest).usersExist(groupInfo.memberIds)
+ doReturn(IO.pure(okGroup)).when(mockGroupRepo).save(any[DB], any[Group])
+ doReturn(IO.pure(Set(okUser.id)))
+ .when(mockMembershipRepo)
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ doReturn(IO.pure(okGroupChange)).when(mockGroupChangeRepo).save(any[DB], any[GroupChange])
+
+ val result = underTest.createGroup(groupInfo.copy(email = "test@test.com"), okAuth).value.unsafeRunSync().toOption.get
+ result shouldBe groupInfo.copy(email = "test@test.com")
+
+ val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
+
+ verify(mockMembershipRepo, times(2))
+ .saveMembers(any[DB], anyString, any[Set[String]], isAdmin = anyBoolean)
+ verify(mockGroupRepo).save(any[DB], groupCaptor.capture())
+
+ val savedGroup = groupCaptor.getValue
+ (savedGroup.memberIds should contain).only(okUser.id)
+ (savedGroup.adminUserIds should contain).only(okUser.id)
+ savedGroup.name shouldBe groupInfo.name
+ savedGroup.email shouldBe groupInfo.copy(email = "test@test.com").email
+ savedGroup.description shouldBe groupInfo.description
+ }
+
"update an existing group" should {
"save the update and add new members and remove deleted members" in {
doReturn(IO.pure(Some(existingGroup))).when(mockGroupRepo).getGroup(any[String])
@@ -304,7 +480,6 @@ class MembershipServiceSpec
.when(mockGroupChangeRepo)
.save(any[DB], any[GroupChange])
- awaitResultOf(
underTest
.updateGroup(
updatedInfo.id,
@@ -315,8 +490,7 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
okAuth
)
- .value
- )
+ .value.unsafeRunSync()
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
val addedMemberCaptor = ArgumentCaptor.forClass(classOf[Set[String]])
@@ -366,7 +540,7 @@ class MembershipServiceSpec
"return an error if the user is not an admin" in {
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroup(anyString)
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -377,8 +551,7 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
dummyAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -392,7 +565,7 @@ class MembershipServiceSpec
.when(underTest)
.differentGroupWithSameNameDoesNotExist(updatedInfo.name, existingGroup.id)
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -403,8 +576,8 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[GroupAlreadyExistsError]
}
@@ -417,7 +590,7 @@ class MembershipServiceSpec
.when(underTest)
.groupValidation(existingGroup.copy(name = "", email = ""))
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -428,15 +601,15 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[GroupValidationError]
}
"return an error if the group is not found" in {
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(existingGroup.id)
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -447,8 +620,8 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[GroupNotFoundError]
}
@@ -463,7 +636,7 @@ class MembershipServiceSpec
.when(underTest)
.usersExist(any[Set[String]])
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -474,8 +647,8 @@ class MembershipServiceSpec
updatedInfo.adminUserIds,
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[UserNotFoundError]
}
@@ -484,7 +657,7 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroup(existingGroup.id)
- val error = leftResultOf(
+ val error =
underTest
.updateGroup(
updatedInfo.id,
@@ -495,8 +668,8 @@ class MembershipServiceSpec
Set(),
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe an[InvalidGroupError]
}
}
@@ -519,7 +692,7 @@ class MembershipServiceSpec
.when(mockZoneRepo)
.getFirstOwnedZoneAclGroupId(anyString())
- val result: Group = rightResultOf(underTest.deleteGroup("ok", okAuth).value)
+ val result: Group = underTest.deleteGroup("ok", okAuth).value.unsafeRunSync().toOption.get
result shouldBe okGroup.copy(status = GroupStatus.Deleted)
val groupCaptor = ArgumentCaptor.forClass(classOf[Group])
@@ -540,7 +713,7 @@ class MembershipServiceSpec
"return an error if the user is not an admin" in {
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroup(anyString)
- val error = leftResultOf(underTest.deleteGroup("ok", dummyAuth).value)
+ val error = underTest.deleteGroup("ok", dummyAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -549,7 +722,7 @@ class MembershipServiceSpec
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(anyString)
doReturn(IO.pure(List())).when(mockZoneRepo).getZonesByAdminGroupId(anyString)
- val error = leftResultOf(underTest.deleteGroup("ok", okAuth).value)
+ val error = underTest.deleteGroup("ok", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[GroupNotFoundError]
}
@@ -560,7 +733,7 @@ class MembershipServiceSpec
.when(mockZoneRepo)
.getZonesByAdminGroupId(anyString)
- val error = leftResultOf(underTest.deleteGroup("ok", okAuth).value)
+ val error = underTest.deleteGroup("ok", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
@@ -570,7 +743,7 @@ class MembershipServiceSpec
doReturn(IO.pure(Some("somerecordsetid")))
.when(mockRecordSetRepo)
.getFirstOwnedRecordByGroup(anyString())
- val error = leftResultOf(underTest.deleteGroup("ok", okAuth).value)
+ val error = underTest.deleteGroup("ok", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
@@ -580,7 +753,7 @@ class MembershipServiceSpec
doReturn(IO.pure(Some("someId")))
.when(mockZoneRepo)
.getFirstOwnedZoneAclGroupId(anyString())
- val error = leftResultOf(underTest.deleteGroup("ok", okAuth).value)
+ val error = underTest.deleteGroup("ok", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
@@ -590,13 +763,13 @@ class MembershipServiceSpec
"get a group" should {
"return the group" in {
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroup(anyString)
- val result: Group = rightResultOf(underTest.getGroup(okGroup.id, okAuth).value)
+ val result: Group = underTest.getGroup(okGroup.id, okAuth).value.unsafeRunSync().toOption.get
result shouldBe okGroup
}
"return an error if the group is not found" in {
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(anyString)
- val error = leftResultOf(underTest.getGroup("notfound", okAuth).value)
+ val error = underTest.getGroup("notfound", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[GroupNotFoundError]
}
}
@@ -607,7 +780,7 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, listOfDummyGroupsAuth, false).value)
+ underTest.listMyGroups(None, None, 100, listOfDummyGroupsAuth, false).value.unsafeRunSync().toOption.get
verify(mockGroupRepo, never()).getAllGroups()
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.take(100),
@@ -622,7 +795,7 @@ class MembershipServiceSpec
doReturn(IO.pure(listOfDummyGroups.toSet))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
- val result: ListMyGroupsResponse = rightResultOf(
+ val result: ListMyGroupsResponse =
underTest
.listMyGroups(
groupNameFilter = Some("name-dummy01"),
@@ -631,8 +804,8 @@ class MembershipServiceSpec
listOfDummyGroupsAuth,
false
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(10, 20),
groupNameFilter = Some("name-dummy01"),
@@ -646,7 +819,7 @@ class MembershipServiceSpec
doReturn(IO.pure(listOfDummyGroups.toSet))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
- val result: ListMyGroupsResponse = rightResultOf(
+ val result: ListMyGroupsResponse =
underTest
.listMyGroups(
groupNameFilter = Some("Name-Dummy01"),
@@ -655,8 +828,8 @@ class MembershipServiceSpec
listOfDummyGroupsAuth,
false
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(10, 20),
groupNameFilter = Some("Name-Dummy01"),
@@ -670,7 +843,7 @@ class MembershipServiceSpec
doReturn(IO.pure(listOfDummyGroups.toSet))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
- val result: ListMyGroupsResponse = rightResultOf(
+ val result: ListMyGroupsResponse =
underTest
.listMyGroups(
groupNameFilter = None,
@@ -679,8 +852,8 @@ class MembershipServiceSpec
listOfDummyGroupsAuth,
ignoreAccess = false
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(100, 200),
groupNameFilter = None,
@@ -694,7 +867,7 @@ class MembershipServiceSpec
doReturn(IO.pure(listOfDummyGroups.toSet))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
- val result: ListMyGroupsResponse = rightResultOf(
+ val result: ListMyGroupsResponse =
underTest
.listMyGroups(
groupNameFilter = None,
@@ -703,8 +876,8 @@ class MembershipServiceSpec
listOfDummyGroupsAuth,
ignoreAccess = false
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe ListMyGroupsResponse(
groups = listOfDummyGroupInfo.slice(0, 10),
groupNameFilter = None,
@@ -717,13 +890,13 @@ class MembershipServiceSpec
"return an empty set if the user is not a member of any groups" in {
doReturn(IO.pure(Set())).when(mockGroupRepo).getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, notAuth, false).value)
+ underTest.listMyGroups(None, None, 100, notAuth, false).value.unsafeRunSync().toOption.get
result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100, false)
}
"return all groups from the database if ignoreAccess is true" in {
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, notAuth, true).value)
+ underTest.listMyGroups(None, None, 100, notAuth, true).value.unsafeRunSync().toOption.get
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -733,7 +906,7 @@ class MembershipServiceSpec
"return all groups from the database for super users even if ignoreAccess is false" in {
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, superUserAuth, false).value)
+ underTest.listMyGroups(None, None, 100, superUserAuth, false).value.unsafeRunSync().toOption.get
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -743,7 +916,7 @@ class MembershipServiceSpec
"return all groups from the database for super users if ignoreAccess is true" in {
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, superUserAuth, true).value)
+ underTest.listMyGroups(None, None, 100, superUserAuth, true).value.unsafeRunSync().toOption.get
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -754,7 +927,7 @@ class MembershipServiceSpec
val supportAuth = AuthPrincipal(okUser.copy(isSupport = true), Seq())
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, supportAuth, false).value)
+ underTest.listMyGroups(None, None, 100, supportAuth, false).value.unsafeRunSync().toOption.get
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -765,7 +938,7 @@ class MembershipServiceSpec
val supportAuth = AuthPrincipal(okUser.copy(isSupport = true), Seq())
doReturn(IO.pure(Set(okGroup, dummyGroup))).when(mockGroupRepo).getAllGroups()
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, supportAuth, true).value)
+ underTest.listMyGroups(None, None, 100, supportAuth, true).value.unsafeRunSync().toOption.get
verify(mockGroupRepo).getAllGroups()
result.groups should contain theSameElementsAs Seq(
GroupInfo(dummyGroup),
@@ -778,7 +951,7 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListMyGroupsResponse =
- rightResultOf(underTest.listMyGroups(None, None, 100, deletedGroupAuth, false).value)
+ underTest.listMyGroups(None, None, 100, deletedGroupAuth, false).value.unsafeRunSync().toOption.get
result shouldBe ListMyGroupsResponse(Seq(), None, None, None, 100, false)
}
}
@@ -799,11 +972,11 @@ class MembershipServiceSpec
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(1).head
val result: GroupChangeInfo =
- rightResultOf(underTest.getGroupChange(dummyGroup.id, dummyAuth).value)
+ underTest.getGroupChange(groupChangeRepoResponse.id, dummyAuth).value.unsafeRunSync().toOption.get
result shouldBe expected
}
- "return the single group change even if the user is not authorized" in {
+ "return an error if the user is not authorized" in {
val groupChangeRepoResponse = listOfDummyGroupChanges.take(1).head
doReturn(IO.pure(Some(groupChangeRepoResponse)))
.when(mockGroupChangeRepo)
@@ -813,13 +986,9 @@ class MembershipServiceSpec
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
- val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
- val expected: GroupChangeInfo =
- listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(1).head
-
- val result: GroupChangeInfo =
- rightResultOf(underTest.getGroupChange(dummyGroup.id, okAuth).value)
- result shouldBe expected
+ val error =
+ underTest.getGroupChange(dummyGroup.id, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[NotAuthorizedError]
}
"return a InvalidGroupRequestError if the group change id is not valid" in {
@@ -831,7 +1000,7 @@ class MembershipServiceSpec
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
- val result = leftResultOf(underTest.getGroupChange(dummyGroup.id, okAuth).value)
+ val result = underTest.getGroupChange(dummyGroup.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidGroupRequestError]
}
}
@@ -840,11 +1009,11 @@ class MembershipServiceSpec
"return the group activity" in {
val groupChangeRepoResponse = ListGroupChangesResults(
listOfDummyGroupChanges.take(100),
- Some(listOfDummyGroupChanges(100).id)
+ Some(listOfDummyGroupChanges.size)
)
doReturn(IO.pure(groupChangeRepoResponse))
.when(mockGroupChangeRepo)
- .getGroupChanges(anyString, any[Option[String]], anyInt)
+ .getGroupChanges(anyString, any[Option[Int]], anyInt)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
@@ -855,47 +1024,49 @@ class MembershipServiceSpec
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
val result: ListGroupChangesResponse =
- rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value)
+ underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value.unsafeRunSync().toOption.get
result.changes should contain theSameElementsAs expected
result.maxItems shouldBe 100
- result.nextId shouldBe Some(listOfDummyGroupChanges(100).id)
+ result.nextId shouldBe Some(listOfDummyGroupChanges.size)
result.startFrom shouldBe None
}
- "return group activity even if the user is not authorized" in {
+ "return an error if the user is not authorized" in {
val groupChangeRepoResponse = ListGroupChangesResults(
listOfDummyGroupChanges.take(100),
- Some(listOfDummyGroupChanges(100).id)
+ Some(listOfDummyGroupChanges.size)
)
doReturn(IO.pure(groupChangeRepoResponse))
.when(mockGroupChangeRepo)
- .getGroupChanges(anyString, any[Option[String]], anyInt)
+ .getGroupChanges(anyString, any[Option[Int]], anyInt)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
- val userMap = Seq(dummyUser).map(u => (u.id, u.userName)).toMap
- val expected: List[GroupChangeInfo] =
- listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
-
- val result: ListGroupChangesResponse =
- rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value)
- result.changes should contain theSameElementsAs expected
- result.maxItems shouldBe 100
- result.nextId shouldBe Some(listOfDummyGroupChanges(100).id)
- result.startFrom shouldBe None
+ val error =
+ underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[NotAuthorizedError]
}
}
+ "get group user ids" should {
+ "get all users in a group change" in {
+ val groupChange = Seq(okGroupChange, dummyGroupChangeUpdate, okGroupChange.copy(changeType = GroupChangeType.Delete))
+ val result: Set[String] = underTest.getGroupUserIds(groupChange)
+ result shouldBe Set("12345-abcde-6789", "56789-edcba-1234", "ok")
+ }
+ }
+
"determine group difference" should {
"return difference between two groups" in {
val groupChange = Seq(okGroupChange, dummyGroupChangeUpdate, okGroupChange.copy(changeType = GroupChangeType.Delete))
- val result: Seq[String] = rightResultOf(underTest.determineGroupDifference(groupChange).value)
+ val allUserMap = Map("ok" -> "ok", "12345-abcde-6789" -> "dummyName", "56789-edcba-1234" -> "super")
+ val result: Seq[String] = underTest.determineGroupDifference(groupChange, allUserMap).value.unsafeRunSync().toOption.get
// Newly created group's change message
result(0) shouldBe "Group Created."
// Updated group's change message
- result(1) shouldBe "Group name changed to 'dummy-group'. Group email changed to 'dummy@test.com'. Group description changed to 'dummy group'. Group admin/s with userId/s (12345-abcde-6789,56789-edcba-1234) added. Group admin/s with userId/s (ok) removed. Group member/s with userId/s (12345-abcde-6789,56789-edcba-1234) added. Group member/s with userId/s (ok) removed."
+ result(1) shouldBe "Group name changed to 'dummy-group'. Group email changed to 'dummy@test.com'. Group description changed to 'dummy group'. Group admin/s with user name/s 'dummyName','super' added. Group admin/s with user name/s 'ok' removed. Group member/s with user name/s 'dummyName','super' added. Group member/s with user name/s 'ok' removed."
// Deleted group's change message
result(2) shouldBe "Group Deleted."
}
@@ -914,7 +1085,7 @@ class MembershipServiceSpec
.getUsers(testGroup.adminUserIds, None, None)
val result: ListAdminsResponse =
- rightResultOf(underTest.listAdmins(testGroup.id, okAuth).value)
+ underTest.listAdmins(testGroup.id, okAuth).value.unsafeRunSync().toOption.get
result.admins should contain theSameElementsAs expectedAdmins
}
@@ -930,7 +1101,7 @@ class MembershipServiceSpec
.getUsers(testGroup.adminUserIds, None, None)
val result: ListAdminsResponse =
- rightResultOf(underTest.listAdmins(testGroup.id, dummyAuth).value)
+ underTest.listAdmins(testGroup.id, dummyAuth).value.unsafeRunSync().toOption.get
result.admins should contain theSameElementsAs expectedAdmins
}
}
@@ -951,7 +1122,7 @@ class MembershipServiceSpec
.getUsers(testGroup.memberIds, None, Some(100))
val result: ListMembersResponse =
- rightResultOf(underTest.listMembers(testGroup.id, None, 100, testAuth).value)
+ underTest.listMembers(testGroup.id, None, 100, testAuth).value.unsafeRunSync().toOption.get
result.members should contain theSameElementsAs expectedMembers
result.nextId shouldBe testListUsersResult.lastEvaluatedId
@@ -976,7 +1147,7 @@ class MembershipServiceSpec
.getUsers(testGroup.memberIds, None, Some(100))
val result: ListMembersResponse =
- rightResultOf(underTest.listMembers(testGroup.id, None, 100, supportAuth).value)
+ underTest.listMembers(testGroup.id, None, 100, supportAuth).value.unsafeRunSync().toOption.get
result.members should contain theSameElementsAs expectedMembers
result.nextId shouldBe testListUsersResult.lastEvaluatedId
@@ -997,7 +1168,7 @@ class MembershipServiceSpec
.getUsers(testGroup.memberIds, None, Some(100))
val result: ListMembersResponse =
- rightResultOf(underTest.listMembers(testGroup.id, None, 100, dummyAuth).value)
+ underTest.listMembers(testGroup.id, None, 100, dummyAuth).value.unsafeRunSync().toOption.get
result.members should contain theSameElementsAs expectedMembers
result.nextId shouldBe testListUsersResult.lastEvaluatedId
@@ -1010,21 +1181,21 @@ class MembershipServiceSpec
"return true when a group with the same name does not exist" in {
doReturn(IO.pure(None)).when(mockGroupRepo).getGroupByName("foo")
- val result = awaitResultOf(underTest.groupWithSameNameDoesNotExist("foo").value)
+ val result = underTest.groupWithSameNameDoesNotExist("foo").value.unsafeRunSync()
result should be(right)
}
"return a GroupAlreadyExistsError if a group with the same name already exists" in {
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroupByName("foo")
- val result = leftResultOf(underTest.groupWithSameNameDoesNotExist("foo").value)
+ val result = underTest.groupWithSameNameDoesNotExist("foo").value.unsafeRunSync().swap.toOption.get
result shouldBe a[GroupAlreadyExistsError]
}
"return true if a group with the same name exists but is deleted" in {
doReturn(IO.pure(Some(deletedGroup))).when(mockGroupRepo).getGroupByName("foo")
- val result = awaitResultOf(underTest.groupWithSameNameDoesNotExist("foo").value)
+ val result = underTest.groupWithSameNameDoesNotExist("foo").value.unsafeRunSync()
result should be(right)
}
}
@@ -1035,7 +1206,7 @@ class MembershipServiceSpec
.when(mockUserRepo)
.getUsers(okGroup.memberIds, None, None)
- val result = awaitResultOf(underTest.usersExist(okGroup.memberIds).value)
+ val result = underTest.usersExist(okGroup.memberIds).value.unsafeRunSync()
result should be(right)
}
@@ -1044,7 +1215,7 @@ class MembershipServiceSpec
.when(mockUserRepo)
.getUsers(Set(okUser.id, dummyUser.id), None, None)
- val result = leftResultOf(underTest.usersExist(Set(okUser.id, dummyUser.id)).value)
+ val result = underTest.usersExist(Set(okUser.id, dummyUser.id)).value.unsafeRunSync().swap.toOption.get
result shouldBe a[UserNotFoundError]
}
}
@@ -1056,7 +1227,7 @@ class MembershipServiceSpec
doReturn(IO.pure(Some(existingGroup))).when(mockGroupRepo).getGroupByName("foo")
val error =
- leftResultOf(underTest.differentGroupWithSameNameDoesNotExist("foo", "bar").value)
+ underTest.differentGroupWithSameNameDoesNotExist("foo", "bar").value.unsafeRunSync().swap.toOption.get
error shouldBe a[GroupAlreadyExistsError]
}
@@ -1064,9 +1235,8 @@ class MembershipServiceSpec
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroupByName(okGroup.name)
- val result = awaitResultOf(
- underTest.differentGroupWithSameNameDoesNotExist(okGroup.name, okGroup.id).value
- )
+ val result =
+ underTest.differentGroupWithSameNameDoesNotExist(okGroup.name, okGroup.id).value.unsafeRunSync()
result should be(right)
}
@@ -1077,9 +1247,8 @@ class MembershipServiceSpec
.when(mockGroupRepo)
.getGroupByName(okGroup.name)
- val result = awaitResultOf(
- underTest.differentGroupWithSameNameDoesNotExist(okGroup.name, okGroup.id).value
- )
+ val result =
+ underTest.differentGroupWithSameNameDoesNotExist(okGroup.name, okGroup.id).value.unsafeRunSync()
result should be(right)
}
}
@@ -1088,7 +1257,7 @@ class MembershipServiceSpec
"return true when a group for deletion is not the admin of a zone" in {
doReturn(IO.pure(List())).when(mockZoneRepo).getZonesByAdminGroupId(okGroup.id)
- val result = awaitResultOf(underTest.isNotZoneAdmin(okGroup).value)
+ val result = underTest.isNotZoneAdmin(okGroup).value.unsafeRunSync()
result should be(right)
}
@@ -1097,7 +1266,7 @@ class MembershipServiceSpec
.when(mockZoneRepo)
.getZonesByAdminGroupId(okGroup.id)
- val error = leftResultOf(underTest.isNotZoneAdmin(okGroup).value)
+ val error = underTest.isNotZoneAdmin(okGroup).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
}
@@ -1106,7 +1275,7 @@ class MembershipServiceSpec
"return true when a group for deletion is not the admin of a zone" in {
doReturn(IO.pure(None)).when(mockRecordSetRepo).getFirstOwnedRecordByGroup(okGroup.id)
- val result = awaitResultOf(underTest.isNotRecordOwnerGroup(okGroup).value)
+ val result = underTest.isNotRecordOwnerGroup(okGroup).value.unsafeRunSync()
result should be(right)
}
@@ -1115,7 +1284,7 @@ class MembershipServiceSpec
.when(mockRecordSetRepo)
.getFirstOwnedRecordByGroup(okGroup.id)
- val error = leftResultOf(underTest.isNotRecordOwnerGroup(okGroup).value)
+ val error = underTest.isNotRecordOwnerGroup(okGroup).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
}
@@ -1124,7 +1293,7 @@ class MembershipServiceSpec
"return successfully when a groupId is not in any zone ACL" in {
doReturn(IO.pure(None)).when(mockZoneRepo).getFirstOwnedZoneAclGroupId(okGroup.id)
- val result = awaitResultOf(underTest.isNotInZoneAclRule(okGroup).value)
+ val result = underTest.isNotInZoneAclRule(okGroup).value.unsafeRunSync()
result should be(right)
}
@@ -1133,7 +1302,7 @@ class MembershipServiceSpec
.when(mockZoneRepo)
.getFirstOwnedZoneAclGroupId(okGroup.id)
- val error = leftResultOf(underTest.isNotInZoneAclRule(okGroup).value)
+ val error = underTest.isNotInZoneAclRule(okGroup).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupRequestError]
}
}
@@ -1176,11 +1345,10 @@ class MembershipServiceSpec
}
"return an error if the signed in user is not a super user" in {
- val error = leftResultOf(
+ val error =
underTest
.updateUserLockStatus(okUser.id, LockStatus.Locked, dummyAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -1190,22 +1358,22 @@ class MembershipServiceSpec
signedInUser = dummyAuth.signedInUser.copy(isSupport = true),
memberGroupIds = Seq.empty
)
- val error = leftResultOf(
+ val error =
underTest
.updateUserLockStatus(okUser.id, LockStatus.Locked, supportAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[NotAuthorizedError]
}
"return an error if the requested user is not found" in {
doReturn(IO.pure(None)).when(mockUserRepo).getUser(okUser.id)
- val error = leftResultOf(
+ val error =
underTest
.updateUserLockStatus(okUser.id, LockStatus.Locked, superUserAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[UserNotFoundError]
}
}
@@ -1213,13 +1381,29 @@ class MembershipServiceSpec
"get user" should {
"return the user" in {
doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUserByIdOrName(anyString)
- val result: User = rightResultOf(underTest.getUser(okUser.id, okAuth).value)
+ val result: User = underTest.getUser(okUser.id, okAuth).value.unsafeRunSync().toOption.get
result shouldBe okUser
}
"return an error if the user is not found" in {
doReturn(IO.pure(None)).when(mockUserRepo).getUserByIdOrName(anyString)
- val error = leftResultOf(underTest.getUser("notfound", okAuth).value)
+ val error = underTest.getUser("notfound", okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[UserNotFoundError]
+ }
+ }
+ "get user info" should {
+ "return the user info" in {
+ doReturn(IO.pure(Some(okUser))).when(mockUserRepo).getUserByIdOrName(anyString)
+ doReturn(IO.pure(Set(okGroup.id))).when(mockMembershipRepo).getGroupsForUser(anyString)
+ val result: UserResponseInfo = underTest.getUserDetails(okUser.id, okAuth).value.unsafeRunSync().toOption.get
+ result.id shouldBe okUser.id
+ result.userName.get shouldBe okUser.userName
+ result.groupId shouldBe Set(okGroup.id)
+ }
+
+ "return an error if the user is not found" in {
+ doReturn(IO.pure(None)).when(mockUserRepo).getUserByIdOrName(anyString)
+ val error = underTest.getUserDetails("notfound", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[UserNotFoundError]
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipValidationsSpec.scala
index 83977ba73..e5159bbd5 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipValidationsSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/membership/MembershipValidationsSpec.scala
@@ -25,6 +25,7 @@ import vinyldns.api.ResultHelpers
import vinyldns.core.TestMembershipData._
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.api.domain.zone.NotAuthorizedError
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.membership.User
class MembershipValidationsSpec
@@ -64,13 +65,13 @@ class MembershipValidationsSpec
canEditGroup(okGroup, superUserAuth) should be(right)
}
"return an error when the user is a support admin only" in {
- val user = User("some", "new", "user", isSupport = true)
+ val user = User("some", "new", Encrypted("user"), isSupport = true)
val supportAuth = AuthPrincipal(user, Seq())
val error = leftValue(canEditGroup(okGroup, supportAuth))
error shouldBe an[NotAuthorizedError]
}
"return an error when the user has no access and is not super" in {
- val user = User("some", "new", "user")
+ val user = User("some", "new", Encrypted("user"))
val nonSuperAuth = AuthPrincipal(user, Seq())
val error = leftValue(canEditGroup(okGroup, nonSuperAuth))
error shouldBe an[NotAuthorizedError]
@@ -85,17 +86,31 @@ class MembershipValidationsSpec
canSeeGroup(okGroup.id, superUserAuth) should be(right)
}
"return true when the user is a support admin" in {
- val user = User("some", "new", "user", isSupport = true)
+ val user = User("some", "new", Encrypted("user"), isSupport = true)
val supportAuth = AuthPrincipal(user, Seq())
canSeeGroup(okGroup.id, supportAuth) should be(right)
}
"return true even when a user is not a member of the group or super" in {
- val user = User("some", "new", "user")
+ val user = User("some", "new", Encrypted("user"))
val nonSuperAuth = AuthPrincipal(user, Seq())
canSeeGroup(okGroup.id, nonSuperAuth) should be(right)
}
}
+ "canSeeGroupChange" should {
+ "return true when the user is in the group" in {
+ canSeeGroup(okGroup.id, okAuth) should be(right)
+ }
+ "return true when the user is a super user" in {
+ canSeeGroup(okGroup.id, superUserAuth) should be(right)
+ }
+ "return true when the user is a support admin" in {
+ val user = User("some", "new", Encrypted("user"), isSupport = true)
+ val supportAuth = AuthPrincipal(user, Seq())
+ canSeeGroup(okGroup.id, supportAuth) should be(right)
+ }
+ }
+
"User toString" should {
"not display access and secret keys" in {
val userString = s"""User: [id="ok"; userName="ok"; firstName="Some(ok)"; lastName="Some(ok)"; email="Some(test@test.com)"; created="${okUser.created}"; isSuper="false"; isSupport="false"; isTest="false"; lockStatus="Unlocked"; ]"""
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala
index 7f704f437..2d76e6f88 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetServiceSpec.scala
@@ -24,8 +24,8 @@ import org.scalatestplus.mockito.MockitoSugar
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
+import vinyldns.api.VinylDNSTestHelpers
import vinyldns.api.config.{ZoneAuthConfigs, DottedHostsConfig}
-import vinyldns.api.{ResultHelpers, VinylDNSTestHelpers}
import vinyldns.api.domain.access.AccessValidations
import vinyldns.api.domain.record.RecordSetHelpers._
import vinyldns.api.domain.zone._
@@ -40,14 +40,16 @@ import vinyldns.core.domain.membership.{GroupRepository, ListUsersResults, UserR
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
import vinyldns.core.queue.MessageQueue
+import vinyldns.core.notifier.{AllNotifiers, Notification, Notifier}
+import scala.concurrent.ExecutionContext
class RecordSetServiceSpec
extends AnyWordSpec
with EitherMatchers
with Matchers
with MockitoSugar
- with ResultHelpers
with BeforeAndAfterEach {
+ private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private val mockZoneRepo = mock[ZoneRepository]
private val mockGroupRepo = mock[GroupRepository]
@@ -59,6 +61,8 @@ class RecordSetServiceSpec
private val mockBackend =
mock[Backend]
private val mockBackendResolver = mock[BackendResolver]
+ private val mockNotifier = mock[Notifier]
+ private val mockNotifiers = AllNotifiers(List(mockNotifier))
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(okZone.id)
doReturn(IO.pure(Some(zoneNotAuthorized)))
@@ -86,7 +90,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
- true
+ true,
+ mockNotifiers
)
val underTestWithDnsBackendValidations = new RecordSetService(
@@ -105,7 +110,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.dottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
- true
+ true,
+ mockNotifiers
)
val underTestWithEmptyDottedHostsConfig = new RecordSetService(
@@ -124,7 +130,8 @@ class RecordSetServiceSpec
VinylDNSTestHelpers.highValueDomainConfig,
VinylDNSTestHelpers.emptyDottedHostsConfig,
VinylDNSTestHelpers.approvedNameServers,
- true
+ true,
+ mockNotifiers
)
def getDottedHostsConfigGroupsAllowed(zone: Zone, config: DottedHostsConfig): List[String] = {
@@ -185,11 +192,12 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
val result: RecordSetChange =
- rightResultOf(
- underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
matches(result.recordSet, record, okZone.name) shouldBe true
result.changeType shouldBe RecordSetChangeType.Create
@@ -199,7 +207,7 @@ class RecordSetServiceSpec
val mockZone = okZone.copy(id = "fakeZone")
doReturn(IO.pure(None)).when(mockZoneRepo).getZone(mockZone.id)
- val result = leftResultOf(underTest.getRecordSetByZone(aaaa.id, mockZone.id, okAuth).value)
+ val result = underTest.getRecordSetByZone(aaaa.id, mockZone.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[ZoneNotFoundError]
}
"fail when the account is not authorized" in {
@@ -207,7 +215,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(aaaa.id)
val result =
- leftResultOf(underTest.getRecordSetByZone(aaaa.id, zoneNotAuthorized.id, okAuth).value)
+ underTest.getRecordSetByZone(aaaa.id, zoneNotAuthorized.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
"fail if the record already exists" in {
@@ -221,7 +229,7 @@ class RecordSetServiceSpec
.when(mockBackend)
.resolve(record.name, okZone.name, record.typ)
- val result = leftResultOf(underTest.addRecordSet(aaaa, okAuth).value)
+ val result = underTest.addRecordSet(aaaa, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[RecordSetAlreadyExists]
}
"fail if the record is dotted and does not satisfy properties in dotted hosts config" in {
@@ -256,7 +264,7 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
+ val result = underTest.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and dotted hosts config is empty" in {
@@ -291,7 +299,7 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result = leftResultOf(underTestWithEmptyDottedHostsConfig.addRecordSet(record, okAuth).value)
+ val result = underTestWithEmptyDottedHostsConfig.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is relative with trailing dot" in {
@@ -327,14 +335,14 @@ class RecordSetServiceSpec
.getUsers(Set.empty, None, None)
val result =
- leftResultOf(underTestWithDnsBackendValidations.addRecordSet(record, okAuth).value)
+ underTestWithDnsBackendValidations.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is a high value domain" in {
val record =
aaaa.copy(name = "high-value-domain", zoneId = okZone.id, status = RecordSetStatus.Active)
- val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
+ val result = underTest.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe InvalidRequest(
HighValueDomainError(s"high-value-domain.${okZone.name}").message
)
@@ -372,9 +380,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
}
@@ -411,9 +418,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
}
@@ -429,9 +435,8 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, record.name)
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
}
@@ -469,13 +474,12 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
}
- "fail if user is in not owner group" in {
+ "fail if user is not in owner group" in {
val record = aaaa.copy(ownerGroupId = Some(dummyGroup.id))
doReturn(IO.pure(List()))
@@ -488,7 +492,7 @@ class RecordSetServiceSpec
.when(mockGroupRepo)
.getGroup(dummyGroup.id)
- val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
+ val result = underTest.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
@@ -505,7 +509,7 @@ class RecordSetServiceSpec
.when(mockGroupRepo)
.getGroup(dummyGroup.id)
- val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
+ val result = underTest.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidGroupError]
}
@@ -544,12 +548,10 @@ class RecordSetServiceSpec
.getUsers(Set.empty, None, None)
val result: RecordSetChange =
- rightResultOf(
underTestWithDnsBackendValidations
.addRecordSet(record, okAuth)
.map(_.asInstanceOf[RecordSetChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
matches(result.recordSet, record, okZone.name) shouldBe true
result.changeType shouldBe RecordSetChangeType.Create
@@ -592,11 +594,13 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
+ doReturn(IO.pure(Some(xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroup(xyzGroup.id)
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe record.name
}
@@ -638,9 +642,8 @@ class RecordSetServiceSpec
.getUsers(xyzGroup.memberIds, None, None)
// passes as all three properties within dotted hosts config (allowed zones, users and record types) are satisfied
- val result: RecordSetChange = rightResultOf(
- underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.addRecordSet(record, xyzAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe record.name
}
@@ -682,7 +685,7 @@ class RecordSetServiceSpec
.getUsers(xyzGroup.memberIds, None, None)
// fails as dotted host record name has dot at the end and is not an apex record
- val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
+ val result = underTest.addRecordSet(record, xyzAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, user, record type is allowed but number of dots allowed in config is 0" in {
@@ -723,7 +726,7 @@ class RecordSetServiceSpec
.getUsers(dummyGroup.memberIds, None, None)
// fails as no.of.dots allowed for the zone in the config is 0
- val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
+ val result = underTest.addRecordSet(record, xyzAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and user, record type is in allowed dotted hosts config except zone" in {
@@ -759,7 +762,7 @@ class RecordSetServiceSpec
.getUsers(Set.empty, None, None)
// fails as only two properties within dotted hosts config (users and record types) are satisfied while zone is not allowed
- val result = leftResultOf(underTest.addRecordSet(record, okAuth).value)
+ val result = underTest.addRecordSet(record, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, record type is in allowed dotted hosts config except user" in {
@@ -798,9 +801,12 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListUsersResults(listOfDummyUsers.toSeq, None)))
.when(mockUserRepo)
.getUsers(dummyGroup.memberIds, None, None)
+ doReturn(IO.pure(Some(abcGroup)))
+ .when(mockGroupRepo)
+ .getGroup(abcGroup.id)
// fails as only two properties within dotted hosts config (zones and record types) are satisfied while user is not allowed
- val result = leftResultOf(underTest.addRecordSet(record, abcAuth).value)
+ val result = underTest.addRecordSet(record, abcAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"fail if the record is dotted and zone, user is in allowed dotted hosts config except record type" in {
@@ -843,7 +849,7 @@ class RecordSetServiceSpec
.getUsers(dummyGroup.memberIds, None, None)
// fails as only two properties within dotted hosts config (zone and user) are satisfied while record type is not allowed
- val result = leftResultOf(underTest.addRecordSet(record, xyzAuth).value)
+ val result = underTest.addRecordSet(record, xyzAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
@@ -852,9 +858,7 @@ class RecordSetServiceSpec
val oldRecord = aaaa.copy(zoneId = okZone.id, status = RecordSetStatus.Active)
val newRecord = oldRecord.copy(ttl = oldRecord.ttl + 1000)
- doReturn(IO.pure(Some(okZone)))
- .when(mockZoneRepo)
- .getZone(okZone.id)
+
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
@@ -883,9 +887,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
matches(result.recordSet, newRecord, okZone.name) shouldBe true
matches(result.updates.get, oldRecord, okZone.name) shouldBe true
@@ -899,9 +902,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(aaaa.copy(zoneId = zoneNotAuthorized.id))))
.when(mockRecordRepo)
.getRecordSet(aaaa.id)
- val result = leftResultOf(
- underTest.updateRecordSet(aaaa.copy(zoneId = zoneNotAuthorized.id), okAuth).value
- )
+ val result =
+ underTest.updateRecordSet(aaaa.copy(zoneId = zoneNotAuthorized.id), okAuth).value.unsafeRunSync().swap.toOption.get
+
result shouldBe a[NotAuthorizedError]
}
"succeed if the dotted record name is unchanged" in {
@@ -940,9 +943,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe oldRecord.name
result.recordSet.ttl shouldBe oldRecord.ttl + 1000
@@ -961,7 +963,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, okAuth).value)
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"succeed if record is apex with dot" in {
@@ -1000,9 +1002,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
result.recordSet.ttl shouldBe oldRecord.ttl + 1000
@@ -1043,9 +1044,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
result.recordSet.ttl shouldBe oldRecord.ttl + 1000
@@ -1086,9 +1086,8 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result: RecordSetChange = rightResultOf(
- underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result: RecordSetChange =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.name shouldBe okZone.name
result.recordSet.ttl shouldBe oldRecord.ttl + 1000
@@ -1109,7 +1108,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, okAuth).value)
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe InvalidRequest(
HighValueDomainError(s"high-value-domain.${okZone.name}").message
)
@@ -1135,7 +1134,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSetsByName(okZone.id, newRecord.name)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, auth).value)
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
"fail if new owner group does not exist" in {
@@ -1164,7 +1163,7 @@ class RecordSetServiceSpec
.when(mockGroupRepo)
.getGroup("doesnt-exist")
- val result = leftResultOf(underTest.updateRecordSet(newRecord, auth).value)
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidGroupError]
}
"fail if user not in new owner group" in {
@@ -1186,6 +1185,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(oldRecord.id)
+ doReturn(IO.pure(Some(newRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, oldRecord.name)
@@ -1193,8 +1195,8 @@ class RecordSetServiceSpec
.when(mockGroupRepo)
.getGroup(okGroup.id)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, auth).value)
- result shouldBe an[InvalidRequest]
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe an[NotAuthorizedError]
}
"succeed if user is in owner group and zone is shared" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
@@ -1242,13 +1244,69 @@ class RecordSetServiceSpec
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
- val result = rightResultOf(
- underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value
- )
+ val result =
+ underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ttl shouldBe newRecord.ttl
result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
}
+ "succeed if user is a superuser and zone is shared and the only record attribute being changed is the record owner group." in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = superUserAuth
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id)
+ )
+
+ val newRecord = oldRecord.copy(ownerGroupId = Some(okGroup.id))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+
+ val result = underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+
+ result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
+ }
+ "fail if user is a superuser and zone is shared and attributes other than record owner group are changed." in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = superUserAuth
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id)
+ )
+
+ val newRecord = oldRecord.copy(ttl = oldRecord.ttl + 1000, ownerGroupId = Some(okGroup.id))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe an[NotAuthorizedError]
+ }
"succeed if user is in owner group and zone is shared and new owner group is none" in {
val zone = okZone.copy(shared = true, id = "test-owner-group")
val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
@@ -1267,6 +1325,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(oldRecord)))
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
+ doReturn(IO.pure(Some(newRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
doReturn(IO.pure(List(oldRecord)))
.when(mockRecordRepo)
.getRecordSetsByName(zone.id, newRecord.name)
@@ -1291,10 +1352,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListUsersResults(Seq(), None)))
.when(mockUserRepo)
.getUsers(Set.empty, None, None)
-
- val result = rightResultOf(
- underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value
- )
+
+ val result =
+ underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
result.recordSet.ttl shouldBe newRecord.ttl
result.recordSet.ownerGroupId shouldBe None
@@ -1312,7 +1372,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, auth).value)
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe an[InvalidRequest]
}
"succeed if new record exists in database but not in DNS backend" in {
@@ -1332,12 +1392,11 @@ class RecordSetServiceSpec
.when(mockBackend)
.resolve(newRecord.name, okZone.name, newRecord.typ)
- val result: RecordSetChange = rightResultOf(
+ val result: RecordSetChange =
underTestWithDnsBackendValidations
.updateRecordSet(newRecord, okAuth)
.map(_.asInstanceOf[RecordSetChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
matches(result.recordSet, newRecord, okZone.name) shouldBe true
matches(result.updates.get, oldRecord, okZone.name) shouldBe true
@@ -1358,7 +1417,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(newRecord.id)
- val result = leftResultOf(underTest.updateRecordSet(newRecord, auth).value)
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[InvalidRequest]
}
}
@@ -1370,12 +1429,11 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(record.id)
- val result: RecordSetChange = rightResultOf(
+ val result: RecordSetChange =
underTest
.deleteRecordSet(record.id, okZone.id, okAuth)
.map(_.asInstanceOf[RecordSetChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
matches(result.recordSet, record, okZone.name) shouldBe true
result.changeType shouldBe RecordSetChangeType.Delete
@@ -1386,7 +1444,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(aaaa.id)
val result =
- leftResultOf(underTest.deleteRecordSet(aaaa.id, zoneNotAuthorized.id, okAuth).value)
+ underTest.deleteRecordSet(aaaa.id, zoneNotAuthorized.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
"fail if the record is a high value domain" in {
@@ -1398,16 +1456,14 @@ class RecordSetServiceSpec
.getRecordSet(record.id)
val result =
- leftResultOf(underTest.deleteRecordSet(record.id, okZone.id, okAuth).value)
+ underTest.deleteRecordSet(record.id, okZone.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe InvalidRequest(
HighValueDomainError(s"high-value-domain.${okZone.name}").message
)
}
"fail for user who is not in record owner group in shared zone" in {
val result =
- leftResultOf(
- underTest.deleteRecordSet(sharedZoneRecord.id, sharedZoneRecord.zoneId, dummyAuth).value
- )
+ underTest.deleteRecordSet(sharedZoneRecord.id, sharedZoneRecord.zoneId, dummyAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
@@ -1417,9 +1473,7 @@ class RecordSetServiceSpec
.getZone(sharedZone.id)
val result =
- leftResultOf(
- underTest.deleteRecordSet(sharedZoneRecord.id, sharedZoneRecord.zoneId, okAuth).value
- )
+ underTest.deleteRecordSet(sharedZoneRecord.id, sharedZoneRecord.zoneId, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
@@ -1468,7 +1522,7 @@ class RecordSetServiceSpec
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val result: RecordSetInfo =
- rightResultOf(underTest.getRecordSet(aaaa.id, okAuth).value)
+ underTest.getRecordSet(aaaa.id, okAuth).value.unsafeRunSync().toOption.get
result shouldBe expectedRecordSetInfo
}
@@ -1479,7 +1533,7 @@ class RecordSetServiceSpec
.when(mockRecordRepo)
.getRecordSet(mockRecord.id)
- val result = leftResultOf(underTest.getRecordSet(mockRecord.id, okAuth).value)
+ val result = underTest.getRecordSet(mockRecord.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[RecordSetNotFoundError]
}
@@ -1497,7 +1551,7 @@ class RecordSetServiceSpec
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val result: RecordSetInfo =
- rightResultOf(underTest.getRecordSetByZone(aaaa.id, okZone.id, okAuth).value)
+ underTest.getRecordSetByZone(aaaa.id, okZone.id, okAuth).value.unsafeRunSync().toOption.get
result shouldBe expectedRecordSetInfo
}
@@ -1509,7 +1563,7 @@ class RecordSetServiceSpec
.getRecordSet(mockRecord.id)
val result =
- leftResultOf(underTest.getRecordSetByZone(mockRecord.id, okZone.id, okAuth).value)
+ underTest.getRecordSetByZone(mockRecord.id, okZone.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[RecordSetNotFoundError]
}
@@ -1523,9 +1577,8 @@ class RecordSetServiceSpec
val expectedRecordSetInfo = RecordSetInfo(sharedZoneRecord, Some(okGroup.name))
val result: RecordSetInfo =
- rightResultOf(
- underTest.getRecordSetByZone(sharedZoneRecord.id, sharedZone.id, okAuth).value
- )
+ underTest.getRecordSetByZone(sharedZoneRecord.id, sharedZone.id, okAuth).value.unsafeRunSync().toOption.get
+
result shouldBe expectedRecordSetInfo
}
@@ -1539,9 +1592,8 @@ class RecordSetServiceSpec
val expectedRecordSetInfo = RecordSetInfo(sharedZoneRecord, None)
val result: RecordSetInfo =
- rightResultOf(
- underTest.getRecordSetByZone(sharedZoneRecord.id, sharedZone.id, sharedAuth).value
- )
+ underTest.getRecordSetByZone(sharedZoneRecord.id, sharedZone.id, sharedAuth).value.unsafeRunSync().toOption.get
+
result shouldBe expectedRecordSetInfo
}
@@ -1553,7 +1605,7 @@ class RecordSetServiceSpec
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val result =
- leftResultOf(underTest.getRecordSetByZone(aaaa.id, zoneNotAuthorized.id, okAuth).value)
+ underTest.getRecordSetByZone(aaaa.id, zoneNotAuthorized.id, okAuth).value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
@@ -1567,11 +1619,10 @@ class RecordSetServiceSpec
val expectedRecordSetInfo = RecordSetInfo(sharedZoneRecordNoOwnerGroup, None)
val result: RecordSetInfo =
- rightResultOf(
underTest
.getRecordSetByZone(sharedZoneRecordNoOwnerGroup.id, sharedZone.id, sharedAuth)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe expectedRecordSetInfo
}
@@ -1583,11 +1634,9 @@ class RecordSetServiceSpec
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(any[String])
val result =
- leftResultOf(
underTest
.getRecordSetByZone(sharedZoneRecordNotApprovedRecordType.id, sharedZone.id, okAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
@@ -1613,11 +1662,10 @@ class RecordSetServiceSpec
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroup(any[String])
- val result = leftResultOf(
+ val result =
underTest
.getRecordSetByZone(notSharedZoneRecordWithOwnerGroup.id, zoneNotAuthorized.id, okAuth)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
result shouldBe a[NotAuthorizedError]
}
}
@@ -1626,12 +1674,12 @@ class RecordSetServiceSpec
"return the group name if a record owner group ID is present" in {
doReturn(IO.pure(Some(okGroup))).when(mockGroupRepo).getGroup(any[String])
- val result = rightResultOf(underTest.getGroupName(Some(okGroup.id)).value)
+ val result = underTest.getGroupName(Some(okGroup.id)).value.unsafeRunSync().toOption.get
result shouldBe Some("ok")
}
"return None if a record owner group ID is not present" in {
- val result = rightResultOf(underTest.getGroupName(None).value)
+ val result = underTest.getGroupName(None).value.unsafeRunSync().toOption.get
result shouldBe None
}
}
@@ -1652,7 +1700,8 @@ class RecordSetServiceSpec
List(sharedZoneRecord),
recordNameFilter = Some("aaaa*"),
nameSort = NameSort.ASC,
- recordOwnerGroupFilter = Some("owner group id")
+ recordOwnerGroupFilter = Some("owner group id") ,
+ recordTypeSort = RecordTypeSort.NONE
)
)
).when(mockRecordRepo)
@@ -1663,10 +1712,11 @@ class RecordSetServiceSpec
recordNameFilter = any[Option[String]],
recordTypeFilter = any[Option[Set[RecordType.RecordType]]],
recordOwnerGroupFilter = any[Option[String]],
- nameSort = any[NameSort.NameSort]
+ nameSort = any[NameSort.NameSort],
+ recordTypeSort = any[RecordTypeSort.RecordTypeSort]
)
- val result: ListGlobalRecordSetsResponse = rightResultOf(
+ val result: ListGlobalRecordSetsResponse =
underTest
.listRecordSets(
startFrom = None,
@@ -1675,10 +1725,11 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = Some("owner group id"),
nameSort = NameSort.ASC,
- authPrincipal = sharedAuth
+ authPrincipal = sharedAuth,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.recordSets shouldBe
List(
RecordSetGlobalInfo(
@@ -1691,7 +1742,7 @@ class RecordSetServiceSpec
}
"fail if recordNameFilter is fewer than two characters" in {
- val result = leftResultOf(
+ val result =
underTest
.listRecordSets(
startFrom = None,
@@ -1700,10 +1751,11 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = Some("owner group id"),
nameSort = NameSort.ASC,
- authPrincipal = okAuth
+ authPrincipal = okAuth,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
result shouldBe an[InvalidRequest]
}
}
@@ -1724,7 +1776,8 @@ class RecordSetServiceSpec
List(sharedZoneRecord),
recordNameFilter = Some("aaaa*"),
nameSort = NameSort.ASC,
- recordOwnerGroupFilter = Some("owner group id")
+ recordOwnerGroupFilter = Some("owner group id"),
+ recordTypeSort = RecordTypeSort.NONE
)
)
).when(mockRecordDataRepo)
@@ -1738,7 +1791,7 @@ class RecordSetServiceSpec
nameSort = any[NameSort.NameSort]
)
- val result = rightResultOf(
+ val result =
underTest
.searchRecordSets(
startFrom = None,
@@ -1747,10 +1800,11 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = Some("owner group id"),
nameSort = NameSort.ASC,
- authPrincipal = sharedAuth
+ authPrincipal = sharedAuth,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.recordSets shouldBe
List(
RecordSetGlobalInfo(
@@ -1763,7 +1817,7 @@ class RecordSetServiceSpec
}
"fail recordSetData if recordNameFilter is fewer than two characters" in {
- val result = leftResultOf(
+ val result =
underTest
.searchRecordSets(
startFrom = None,
@@ -1772,10 +1826,11 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = Some("owner group id"),
nameSort = NameSort.ASC,
- authPrincipal = okAuth
+ authPrincipal = okAuth,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
result shouldBe an[InvalidRequest]
}
}
@@ -1791,7 +1846,8 @@ class RecordSetServiceSpec
IO.pure(
ListRecordSetResults(
List(sharedZoneRecord, sharedZoneRecordNotFoundOwnerGroup),
- nameSort = NameSort.ASC
+ nameSort = NameSort.ASC,
+ recordTypeSort = RecordTypeSort.ASC
)
)
).when(mockRecordRepo)
@@ -1802,10 +1858,11 @@ class RecordSetServiceSpec
recordNameFilter = None,
recordTypeFilter = None,
recordOwnerGroupFilter = None,
- nameSort = NameSort.ASC
+ nameSort = NameSort.ASC,
+ recordTypeSort = RecordTypeSort.ASC
)
- val result: ListRecordSetsByZoneResponse = rightResultOf(
+ val result: ListRecordSetsByZoneResponse =
underTest
.listRecordSetsByZone(
sharedZone.id,
@@ -1815,10 +1872,11 @@ class RecordSetServiceSpec
authPrincipal = sharedAuth,
recordTypeFilter = None,
recordOwnerGroupFilter = None,
- nameSort = NameSort.ASC
+ nameSort = NameSort.ASC,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.recordSets shouldBe
List(
RecordSetListInfo(
@@ -1836,7 +1894,7 @@ class RecordSetServiceSpec
.when(mockGroupRepo)
.getGroups(Set())
- doReturn(IO.pure(ListRecordSetResults(List(aaaa), nameSort = NameSort.ASC)))
+ doReturn(IO.pure(ListRecordSetResults(List(aaaa), nameSort = NameSort.ASC, recordTypeSort = RecordTypeSort.NONE)))
.when(mockRecordRepo)
.listRecordSets(
zoneId = Some(okZone.id),
@@ -1845,10 +1903,11 @@ class RecordSetServiceSpec
recordNameFilter = None,
recordTypeFilter = None,
recordOwnerGroupFilter = None,
- nameSort = NameSort.ASC
+ nameSort = NameSort.ASC,
+ recordTypeSort = RecordTypeSort.ASC
)
- val result: ListRecordSetsByZoneResponse = rightResultOf(
+ val result: ListRecordSetsByZoneResponse =
underTest
.listRecordSetsByZone(
okZone.id,
@@ -1858,16 +1917,17 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = None,
nameSort = NameSort.ASC,
- authPrincipal = AuthPrincipal(okAuth.signedInUser.copy(isSupport = true), Seq.empty)
+ authPrincipal = AuthPrincipal(okAuth.signedInUser.copy(isSupport = true), Seq.empty),
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result.recordSets shouldBe List(
RecordSetListInfo(RecordSetInfo(aaaa, None), AccessLevel.Read)
)
}
"fails when the account is not authorized" in {
- val result = leftResultOf(
+ val result =
underTest
.listRecordSetsByZone(
zoneNotAuthorized.id,
@@ -1877,10 +1937,11 @@ class RecordSetServiceSpec
recordTypeFilter = None,
recordOwnerGroupFilter = None,
nameSort = NameSort.ASC,
- authPrincipal = okAuth
+ authPrincipal = okAuth,
+ recordTypeSort = RecordTypeSort.ASC
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
+
result shouldBe a[NotAuthorizedError]
}
}
@@ -1890,19 +1951,51 @@ class RecordSetServiceSpec
val completeRecordSetChanges: List[RecordSetChange] =
List(pendingCreateAAAA, pendingCreateCNAME, completeCreateAAAA, completeCreateCNAME)
+ doReturn(IO.pure(Some(zoneActive)))
+ .when(mockZoneRepo)
+ .getZone(zoneActive.id)
doReturn(IO.pure(ListRecordSetChangesResults(completeRecordSetChanges)))
.when(mockRecordChangeRepo)
- .listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
+ .listRecordSetChanges(zoneId = Some(zoneActive.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse =
- rightResultOf(underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value)
+ underTest.listRecordSetChanges(zoneActive.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val changesWithName =
completeRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
val expectedResults = ListRecordSetChangesResponse(
- zoneId = okZone.id,
+ zoneId = zoneActive.id,
+ recordSetChanges = changesWithName,
+ nextId = None,
+ startFrom = None,
+ maxItems = 100
+ )
+ result shouldBe expectedResults
+ }
+
+ "retrieve the recordset changes based on fqdn and record type" in {
+ val filteredRecordSetChanges: List[RecordSetChange] =
+ List(pendingCreateAAAA, completeCreateAAAA)
+ val zoneId = filteredRecordSetChanges.head.zoneId
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(zoneId)
+ doReturn(IO.pure(ListRecordSetChangesResults(filteredRecordSetChanges)))
+ .when(mockRecordChangeRepo)
+ .listRecordSetChanges(zoneId = Some(zoneId), startFrom = None, maxItems = 100, fqdn = Some("aaaa.ok.zone.recordsets."), recordType = Some(RecordType.AAAA))
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListRecordSetHistoryResponse =
+ underTest.listRecordSetChangeHistory(zoneId = Some(zoneId), fqdn = Some("aaaa.ok.zone.recordsets."), recordType = Some(RecordType.AAAA), authPrincipal = okAuth).value.unsafeRunSync().toOption.get
+ val changesWithName =
+ filteredRecordSetChanges.map(change => RecordSetChangeInfo(change, Some("ok")))
+ val expectedResults = ListRecordSetHistoryResponse(
+ zoneId = Some(zoneId),
recordSetChanges = changesWithName,
nextId = None,
startFrom = None,
@@ -1914,10 +2007,13 @@ class RecordSetServiceSpec
"return a zone with no changes if no changes exist" in {
doReturn(IO.pure(ListRecordSetChangesResults(items = Nil)))
.when(mockRecordChangeRepo)
- .listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
+ .listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse =
- rightResultOf(underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value)
+ underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val expectedResults = ListRecordSetChangesResponse(
zoneId = okZone.id,
recordSetChanges = List(),
@@ -1928,10 +2024,63 @@ class RecordSetServiceSpec
result shouldBe expectedResults
}
+ "listFailedRecordSetChanges" should {
+ "retrieve the recordset changes with default maxItems and startFrom" in {
+ val completeRecordSetChanges: List[RecordSetChange] = List(
+ pendingCreateAAAA.copy(status = RecordSetChangeStatus.Failed),
+ pendingCreateCNAME.copy(status = RecordSetChangeStatus.Failed),
+ completeCreateAAAA.copy(status = RecordSetChangeStatus.Failed),
+ completeCreateCNAME.copy(status = RecordSetChangeStatus.Failed)
+ )
+
+ doReturn(IO.pure(ListFailedRecordSetChangesResults(completeRecordSetChanges)))
+ .when(mockRecordChangeRepo)
+ .listFailedRecordSetChanges(Some(okZone.id),100,0)
+
+
+ val result: ListFailedRecordSetChangesResponse =
+ underTest.listFailedRecordSetChanges(authPrincipal = okAuth,Some(okZone.id)).value.unsafeRunSync().toOption.get
+
+ val changesWithName =
+ ListFailedRecordSetChangesResponse(
+ completeRecordSetChanges,
+ nextId = 0,
+ startFrom = 0,
+ maxItems = 100)
+
+ result shouldBe changesWithName
+ }
+ "retrieve the recordset changes with maxItems 3 and startFrom 2" in {
+ val completeRecordSetChanges: List[RecordSetChange] = List(
+ pendingCreateAAAA.copy(status = RecordSetChangeStatus.Failed),
+ pendingCreateCNAME.copy(status = RecordSetChangeStatus.Failed),
+ completeCreateAAAA.copy(status = RecordSetChangeStatus.Failed),
+ completeCreateCNAME.copy(status = RecordSetChangeStatus.Failed)
+ )
+
+ doReturn(IO.pure(ListFailedRecordSetChangesResults(completeRecordSetChanges)))
+ .when(mockRecordChangeRepo)
+ .listFailedRecordSetChanges(Some(okZone.id),3,2)
+
+
+ val result: ListFailedRecordSetChangesResponse =
+ underTest.listFailedRecordSetChanges(authPrincipal = okAuth,Some(okZone.id),2,3).value.unsafeRunSync().toOption.get
+
+ val changesWithName =
+ ListFailedRecordSetChangesResponse(
+ completeRecordSetChanges,
+ nextId = 0,
+ startFrom = 2,
+ maxItems = 3)
+
+ result shouldBe changesWithName
+ }
+ }
+
"return a NotAuthorizedError" in {
- val error = leftResultOf(
- underTest.listRecordSetChanges(zoneNotAuthorized.id, authPrincipal = okAuth).value
- )
+ val error =
+ underTest.listRecordSetChanges(zoneNotAuthorized.id, authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
+
error shouldBe a[NotAuthorizedError]
}
@@ -1941,10 +2090,13 @@ class RecordSetServiceSpec
doReturn(IO.pure(ListRecordSetChangesResults(List(rsChange2, rsChange1))))
.when(mockRecordChangeRepo)
- .listRecordSetChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
+ .listRecordSetChanges(zoneId = Some(okZone.id), startFrom = None, maxItems = 100, fqdn = None, recordType = None)
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result: ListRecordSetChangesResponse =
- rightResultOf(underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value)
+ underTest.listRecordSetChanges(okZone.id, authPrincipal = okAuth).value.unsafeRunSync().toOption.get
val changesWithName =
List(RecordSetChangeInfo(rsChange2, Some("ok")), RecordSetChangeInfo(rsChange1, Some("ok")))
val expectedResults = ListRecordSetChangesResponse(
@@ -1965,7 +2117,7 @@ class RecordSetServiceSpec
.getRecordSetChange(okZone.id, pendingCreateAAAA.id)
val actual: RecordSetChange =
- rightResultOf(underTest.getRecordSetChange(okZone.id, pendingCreateAAAA.id, okAuth).value)
+ underTest.getRecordSetChange(okZone.id, pendingCreateAAAA.id, okAuth).value.unsafeRunSync().toOption.get
actual shouldBe pendingCreateAAAA
}
@@ -1975,9 +2127,8 @@ class RecordSetServiceSpec
.getRecordSetChange(sharedZone.id, pendingCreateSharedRecord.id)
val actual: RecordSetChange =
- rightResultOf(
- underTest.getRecordSetChange(sharedZone.id, pendingCreateSharedRecord.id, okAuth).value
- )
+ underTest.getRecordSetChange(sharedZone.id, pendingCreateSharedRecord.id, okAuth).value.unsafeRunSync().toOption.get
+
actual shouldBe pendingCreateSharedRecord
}
@@ -1986,19 +2137,44 @@ class RecordSetServiceSpec
.when(mockRecordChangeRepo)
.getRecordSetChange(okZone.id, pendingCreateAAAA.id)
val error =
- leftResultOf(underTest.getRecordSetChange(okZone.id, pendingCreateAAAA.id, okAuth).value)
+ underTest.getRecordSetChange(okZone.id, pendingCreateAAAA.id, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[RecordSetChangeNotFoundError]
}
+ "return a RecordSets Count" in {
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(okZone.id)
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+ doReturn(IO.pure(10))
+ .when(mockRecordRepo).getRecordSetCount(okZone.id)
+
+ val result = underTest.getRecordSetCount(okZone.id,authPrincipal = okAuth).value.unsafeRunSync().toOption.get
+ result shouldBe RecordSetCount(10)
+
+ }
+
+ "return a NotAuthorizedError for getRecordSetCount if the user is not authorized to access the zone" in {
+ doReturn(IO.pure(10))
+ .when(mockRecordRepo).getRecordSetCount(zoneNotAuthorized.id)
+ val error =
+ underTest.getRecordSetCount((zoneNotAuthorized.id), authPrincipal = okAuth).value.unsafeRunSync().swap.toOption.get
+
+ error shouldBe a[NotAuthorizedError]
+
+ }
+
+
"return a NotAuthorizedError if the user is not authorized to access the zone" in {
doReturn(IO.pure(Some(zoneActive))).when(mockZoneRepo).getZone(zoneActive.id)
doReturn(IO.pure(Some(pendingCreateAAAA)))
.when(mockRecordChangeRepo)
.getRecordSetChange(zoneActive.id, pendingCreateAAAA.id)
- val error = leftResultOf(
- underTest.getRecordSetChange(zoneActive.id, pendingCreateAAAA.id, dummyAuth).value
- )
+ val error =
+ underTest.getRecordSetChange(zoneActive.id, pendingCreateAAAA.id, dummyAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -2009,15 +2185,14 @@ class RecordSetServiceSpec
.when(mockRecordChangeRepo)
.getRecordSetChange(zoneNotAuthorized.id, pendingCreateSharedRecordNotSharedZone.id)
- val error = leftResultOf(
+ val error =
underTest
.getRecordSetChange(
zoneNotAuthorized.id,
pendingCreateSharedRecordNotSharedZone.id,
okAuth
)
- .value
- )
+ .value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -2025,18 +2200,528 @@ class RecordSetServiceSpec
"formatRecordNameFilter" should {
"return an FQDN from an IPv4 address" in {
- rightResultOf(underTest.formatRecordNameFilter("10.10.0.25").value) shouldBe
+ underTest.formatRecordNameFilter("10.10.0.25").value.unsafeRunSync().toOption.get shouldBe
"25.0.10.10.in-addr.arpa."
}
"return an FQDN from an IPv6 address" in {
- rightResultOf(underTest.formatRecordNameFilter("10.10.0.25").value) shouldBe
+ underTest.formatRecordNameFilter("10.10.0.25").value.unsafeRunSync().toOption.get shouldBe
"25.0.10.10.in-addr.arpa."
}
"return a string with a trailing dot" in {
- rightResultOf(underTest.formatRecordNameFilter("thing.com").value) shouldBe
+ underTest.formatRecordNameFilter("thing.com").value.unsafeRunSync().toOption.get shouldBe
"thing.com."
}
}
+ "ownerShipTransfer" should {
+ "success if user request AutoApproved for the owner group is null" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = None
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved,requestedOwnerGroupId = Some(okGroup.id))))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
+ .when(mockZoneRepo)
+ .getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(None))
+ .when(mockZoneRepo)
+ .getZoneByName(newRecord.name + "." + okZone.name)
+ doReturn(IO.pure(List()))
+ .when(mockRecordRepo)
+ .getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(Set()))
+ .when(mockGroupRepo)
+ .getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
+ doReturn(IO.pure(ListUsersResults(Seq(), None)))
+ .when(mockUserRepo)
+ .getUsers(Set.empty, None, None)
+
+ val result =
+ underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+
+ result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.AutoApproved)
+ }
+
+ "fail if user request ownership transfer for non shared zone" in {
+ val zone = okZone.copy(id = "test-owner-group")
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id)
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested, requestedOwnerGroupId = Some(okGroup.id))))
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, oldRecord.name)
+
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe an[InvalidRequest]
+ }
+
+ "fail if user not a member of owner group and tried to Approve ownership transfer request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, oldRecord.name)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe an[InvalidRequest]
+ }
+
+ "fail if user not a member of owner group and tried to Reject ownership transfer request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, oldRecord.name)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe an[InvalidRequest]
+ }
+
+ "success if user not a member of owner group and tried to Request ownership transfer request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id)
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested,requestedOwnerGroupId = Some(okGroup.id))))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
+ .when(mockZoneRepo)
+ .getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(None))
+ .when(mockZoneRepo)
+ .getZoneByName(newRecord.name + "." + okZone.name)
+ doReturn(IO.pure(List()))
+ .when(mockRecordRepo)
+ .getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(Set()))
+ .when(mockGroupRepo)
+ .getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
+ doReturn(IO.pure(ListUsersResults(Seq(), None)))
+ .when(mockUserRepo)
+ .getUsers(Set.empty, None, None)
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+
+ result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.PendingReview)
+ }
+
+ "success if user not a member of owner group and tried to Cancel ownership transfer request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
+ .when(mockZoneRepo)
+ .getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(None))
+ .when(mockZoneRepo)
+ .getZoneByName(newRecord.name + "." + okZone.name)
+ doReturn(IO.pure(List()))
+ .when(mockRecordRepo)
+ .getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(Set()))
+ .when(mockGroupRepo)
+ .getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
+ doReturn(IO.pure(ListUsersResults(Seq(), None)))
+ .when(mockUserRepo)
+ .getUsers(Set.empty, None, None)
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result =
+ underTest.updateRecordSet(newRecord, okAuth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+
+ result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.Cancelled)
+ }
+
+ "fail if user not a member of owner group and tried to update ttl while requesting ownership transfer" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord =
+ aaaa.copy(zoneId = zone.id, status = RecordSetStatus.Active, ownerGroupId = Some(oneUserDummyGroup.id))
+ val newRecord = oldRecord.copy(
+ ttl = oldRecord.ttl + 1000,
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Requested,requestedOwnerGroupId = Some(okGroup.id))))
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(newRecord.zoneId)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe a[InvalidRequest]
+ }
+
+ "fail if user not a member of owner group and tried to update ttl while cancel ownership transfer" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val oldRecord =
+ aaaa.copy(zoneId = zone.id, status = RecordSetStatus.Active, ownerGroupId = Some(oneUserDummyGroup.id),recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(okGroup.id)))
+ )
+ val newRecord = oldRecord.copy(
+ records = List(AAAAData("1:2:3:4:5:6:7:9")),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled)))
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(newRecord.zoneId)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+
+ val result = underTest.updateRecordSet(newRecord, okAuth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe a[InvalidRequest]
+ }
+
+ "fail if user cancelled the ownership request and group member tried to approve the request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(newRecord.zoneId)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe a[InvalidRequest]
+ }
+
+ "fail if user cancelled the ownership request and group member tried to reject the request" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(newRecord.zoneId)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe a[InvalidRequest]
+ }
+
+ "fail if user cancelled the ownership request and group member tried to reject the auto-approve" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-failure",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved)))
+
+ doReturn(IO.pure(Some(okZone)))
+ .when(mockZoneRepo)
+ .getZone(newRecord.zoneId)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+
+ val result = underTest.updateRecordSet(newRecord, auth).value.unsafeRunSync().swap.toOption.get
+ result shouldBe a[InvalidRequest]
+ }
+
+ "success if user Approve a ownership transfer request Manually" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved)))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
+ .when(mockZoneRepo)
+ .getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(None))
+ .when(mockZoneRepo)
+ .getZoneByName(newRecord.name + "." + okZone.name)
+ doReturn(IO.pure(List()))
+ .when(mockRecordRepo)
+ .getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(Set()))
+ .when(mockGroupRepo)
+ .getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
+ doReturn(IO.pure(ListUsersResults(Seq(), None)))
+ .when(mockUserRepo)
+ .getUsers(Set.empty, None, None)
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = {
+ underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+ }
+
+ result.recordSet.ownerGroupId shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.ManuallyApproved)
+ }
+
+ "success if user Reject a ownership transfer request Manually" in {
+ val zone = okZone.copy(shared = true, id = "test-owner-group")
+ val auth = AuthPrincipal(listOfDummyUsers.head, Seq(oneUserDummyGroup.id))
+ val oldRecord = aaaa.copy(
+ name = "test-owner-group-success",
+ zoneId = zone.id,
+ status = RecordSetStatus.Active,
+ ownerGroupId = Some(oneUserDummyGroup.id),
+ recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,requestedOwnerGroupId = Some(okGroup.id)))
+ )
+
+ val newRecord = oldRecord.copy(recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected)))
+
+ doReturn(IO.pure(Some(zone)))
+ .when(mockZoneRepo)
+ .getZone(zone.id)
+ doReturn(IO.pure(Some(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSet(newRecord.id)
+ doReturn(IO.pure(List(oldRecord)))
+ .when(mockRecordRepo)
+ .getRecordSetsByName(zone.id, newRecord.name)
+ doReturn(IO.pure(Some(oneUserDummyGroup)))
+ .when(mockGroupRepo)
+ .getGroup(oneUserDummyGroup.id)
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepo)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Set(dottedZone, abcZone, xyzZone, dotZone)))
+ .when(mockZoneRepo)
+ .getZonesByNames(dottedHostsConfigZonesAllowed.toSet)
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(None))
+ .when(mockZoneRepo)
+ .getZoneByName(newRecord.name + "." + okZone.name)
+ doReturn(IO.pure(List()))
+ .when(mockRecordRepo)
+ .getRecordSetsByFQDNs(Set(newRecord.name + "." + okZone.name))
+ doReturn(IO.pure(Set()))
+ .when(mockZoneRepo)
+ .getZonesByFilters(Set.empty)
+ doReturn(IO.pure(Set()))
+ .when(mockGroupRepo)
+ .getGroupsByName(dottedHostsConfigGroupsAllowed.toSet)
+ doReturn(IO.pure(ListUsersResults(Seq(), None)))
+ .when(mockUserRepo)
+ .getUsers(Set.empty, None, None)
+ doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
+
+ val result = {
+ underTest.updateRecordSet(newRecord, auth).map(_.asInstanceOf[RecordSetChange]).value.unsafeRunSync().toOption.get
+ }
+
+ result.recordSet.ownerGroupId shouldBe Some(oneUserDummyGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get) shouldBe Some(okGroup.id)
+ result.recordSet.recordSetGroupChange.map(_.ownerShipTransferStatus) shouldBe Some(OwnerShipTransferStatus.ManuallyRejected)
+ }
+ }
}
+
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala
index 703dc386e..b104f0c3f 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/record/RecordSetValidationsSpec.scala
@@ -486,6 +486,10 @@ class RecordSetValidationsSpec
val error = leftValue(cnameValidations(invalid, List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
}
+ "return an InvalidRequest if a cname record set fqdn is IPv4 address" in {
+ val error = leftValue(cnameValidations(cname.copy(records = List(CNAMEData(Fqdn("1.2.3.4")))), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
+ error shouldBe an[RecordSetValidation]
+ }
"return an InvalidRequest if a cname record set name is dotted" in {
val error = leftValue(cnameValidations(cname.copy(name = "dot.ted"), List(), okZone, None, true, dottedHostsConfigZonesAllowed.toSet, false))
error shouldBe an[InvalidRequest]
@@ -688,5 +692,115 @@ class RecordSetValidationsSpec
error.getMessage() shouldBe RecordNameFilterError
}
}
+
+ "canSuperUserUpdateOwnerGroup" should {
+ "return true when record owner group is the only field changed in the updated record, the zone is shared, " +
+ "and user is a superuser" in {
+ val zone = sharedZone
+ val existing = sharedZoneRecord.copy(ownerGroupId = Some(okGroup.id))
+ val rs = sharedZoneRecord.copy(ownerGroupId = Some(dummyGroup.id))
+ canSuperUserUpdateOwnerGroup(existing, rs, zone, superUserAuth) should be(true)
+ }
+ "return false when record owner group is the only field changed in the updated record, the zone is shared, " +
+ "and user is NOT a superuser" in {
+ val zone = sharedZone
+ val existing = sharedZoneRecord.copy(ownerGroupId = Some(okGroup.id))
+ val rs = sharedZoneRecord.copy(ownerGroupId = Some(dummyGroup.id))
+ canSuperUserUpdateOwnerGroup(existing, rs, zone, okAuth) should be(false)
+ }
+ "return false when record owner group is the only field changed in the updated record, the zone is NOT shared, " +
+ "and user is a superuser" in {
+ val zone = okZone
+ val existing = sharedZoneRecord.copy(ownerGroupId = Some(okGroup.id))
+ val rs = sharedZoneRecord.copy(ownerGroupId = Some(dummyGroup.id))
+ canSuperUserUpdateOwnerGroup(existing, rs, zone, superUserAuth) should be(false)
+ }
+ "return false when record owner group is NOT the only field changed in the updated record" in {
+ val zone = sharedZone
+ val existing = sharedZoneRecord.copy(ownerGroupId = Some(okGroup.id), records = List(AData("10.1.1.1")))
+ val rs = sharedZoneRecord.copy(ownerGroupId = Some(dummyGroup.id), records = List(AData("10.1.1.2")))
+ canSuperUserUpdateOwnerGroup(existing, rs, zone, superUserAuth) should be(false)
+ }
+ }
+ "unchangedRecordSet" should {
+ "return invalid request when given zone ID does not match existing recordset zone ID" in {
+ val existing = rsOk
+ val rs = rsOk.copy(zoneId = "not-real")
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given record type does not match existing recordset record type" in {
+ val existing = rsOk
+ val rs = rsOk.copy(typ = RecordType.AAAA)
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given records does not match existing recordset records" in {
+ val existing = rsOk
+ val rs = rsOk.copy(records = List(AData("10.1.1.0")))
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given recordset id does not match existing recordset ID" in {
+ val existing = rsOk
+ val rs = rsOk.copy(id = abcRecord.id)
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given recordset name does not match existing recordset name" in {
+ val existing = rsOk
+ val rs = rsOk.copy(name = "abc")
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given owner group ID does not match existing recordset owner group ID" in {
+ val existing = rsOk
+ val rs = rsOk.copy(ownerGroupId = Some(abcGroup.id))
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ "return invalid request when given ttl does not match existing recordset ttl" in {
+ val existing = rsOk
+ val rs = rsOk.copy(ttl = 3000)
+ val error = leftValue(unchangedRecordSet(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet's if user not a member of ownership group. User can only request for ownership transfer"
+ }
+ }
+ "recordSetOwnerShipApproveStatus" should {
+ "return invalid request when given ownership transfer does not match OwnerShipTransferStatus as ManuallyRejected" in {
+ val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.ManuallyRejected)))
+ val error = leftValue(recordSetOwnerShipApproveStatus(rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
+ }
+ "return invalid request when given ownership transfer does not match OwnerShipTransferStatus as ManuallyApproved" in {
+ val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.ManuallyApproved)))
+ val error = leftValue(recordSetOwnerShipApproveStatus(rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
+ }
+ "return invalid request when given ownership transfer does not match OwnerShipTransferStatus as AutoApproved" in {
+ val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.AutoApproved)))
+ val error = leftValue(recordSetOwnerShipApproveStatus(rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when request is cancelled."
+ }
+ }
+ "unchangedRecordSetOwnershipStatus" should {
+ "return invalid request when given ownership transfer status does not match existing recordset ownership transfer status for non shared zones" in {
+ val existing = rsOk
+ val rs = rsOk.copy(recordSetGroupChange = Some(ownerShipTransfer.copy(OwnerShipTransferStatus.AutoApproved)))
+ val error = leftValue(unchangedRecordSetOwnershipStatus(existing, rs))
+ error shouldBe an[InvalidRequest]
+ error.getMessage() shouldBe "Cannot update RecordSet OwnerShip Status when zone is not shared."
+ }
+ }
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneConnectionValidatorSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneConnectionValidatorSpec.scala
index 01dc10bbb..838eb2d24 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneConnectionValidatorSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneConnectionValidatorSpec.scala
@@ -25,10 +25,9 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import vinyldns.core.domain.record._
-import vinyldns.api.ResultHelpers
import cats.effect._
import org.mockito.Matchers.any
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.domain.backend.{Backend, BackendResolver}
import vinyldns.core.domain.zone.{ConfiguredDnsConnections, LegacyDnsBackend, Zone, ZoneConnection}
@@ -40,7 +39,6 @@ class ZoneConnectionValidatorSpec
with Matchers
with MockitoSugar
with BeforeAndAfterEach
- with ResultHelpers
with EitherMatchers
with EitherValues {
@@ -86,9 +84,9 @@ class ZoneConnectionValidatorSpec
"vinyldns.",
"test@test.com",
connection =
- Some(ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")),
+ Some(ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")),
transferConnection =
- Some(ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1"))
+ Some(ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1"))
)
private val successSoa = RecordSet(
@@ -135,8 +133,8 @@ class ZoneConnectionValidatorSpec
List(NSData(Fqdn("sub.some.test.ns.")))
)
- val zc = ZoneConnection("zc.", "zc.", "zc", "10.1.1.1")
- val transfer = ZoneConnection("transfer.", "transfer.", "transfer", "10.1.1.1")
+ val zc = ZoneConnection("zc.", "zc.", Encrypted("zc"), "10.1.1.1")
+ val transfer = ZoneConnection("transfer.", "transfer.", Encrypted("transfer"), "10.1.1.1")
val backend = LegacyDnsBackend(
"some-backend-id",
zc.copy(name = "backend-conn"),
@@ -153,7 +151,7 @@ class ZoneConnectionValidatorSpec
doReturn(IO.pure(true)).when(mockBackend).zoneExists(any[Zone])
doReturn(mockBackend).when(mockBackendResolver).resolve(any[Zone])
- val result = awaitResultOf(underTest.validateZoneConnections(testZone).value)
+ val result = underTest.validateZoneConnections(testZone).value.unsafeRunSync()
result should be(right)
}
@@ -165,7 +163,7 @@ class ZoneConnectionValidatorSpec
doReturn(IO.pure(true)).when(mockBackend).zoneExists(any[Zone])
doReturn(mockBackend).when(mockBackendResolver).resolve(any[Zone])
- val result = leftResultOf(underTest.validateZoneConnections(testZone).value)
+ val result = underTest.validateZoneConnections(testZone).value.unsafeRunSync().swap.toOption.get
result shouldBe ZoneValidationFailed(
testZone,
List(s"Name Server not.approved. is not an approved name server."),
@@ -182,7 +180,7 @@ class ZoneConnectionValidatorSpec
doReturn(IO.pure(true)).when(mockBackend).zoneExists(any[Zone])
doReturn(mockBackend).when(mockBackendResolver).resolve(any[Zone])
- val result = leftResultOf(underTest.validateZoneConnections(testZone).value)
+ val result = underTest.validateZoneConnections(testZone).value.unsafeRunSync().swap.toOption.get
result shouldBe a[ZoneValidationFailed]
result shouldBe ZoneValidationFailed(
testZone,
@@ -196,14 +194,14 @@ class ZoneConnectionValidatorSpec
"error.",
"test@test.com",
connection =
- Some(ZoneConnection("error.", "error.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")),
+ Some(ZoneConnection("error.", "error.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")),
transferConnection =
- Some(ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1"))
+ Some(ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1"))
)
doReturn(IO.pure(true)).when(mockBackend).zoneExists(any[Zone])
doReturn(mockBackend).when(mockBackendResolver).resolve(any[Zone])
- val result = leftResultOf(underTest.validateZoneConnections(badZone).value)
+ val result = underTest.validateZoneConnections(badZone).value.unsafeRunSync().swap.toOption.get
result shouldBe a[ConnectionFailed]
}
@@ -212,15 +210,15 @@ class ZoneConnectionValidatorSpec
"error.",
"test@test.com",
connection =
- Some(ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")),
+ Some(ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")),
transferConnection =
- Some(ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1"))
+ Some(ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1"))
)
doReturn(IO.pure(true)).when(mockBackend).zoneExists(any[Zone])
doReturn(mockBackend).when(mockBackendResolver).resolve(any[Zone])
- val result = leftResultOf(underTest.validateZoneConnections(badZone).value)
+ val result = underTest.validateZoneConnections(badZone).value.unsafeRunSync().swap.toOption.get
result shouldBe a[ConnectionFailed]
result.getMessage should include("transfer connection failure!")
}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala
index 69faad71a..d45198ee5 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneServiceSpec.scala
@@ -23,10 +23,13 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.mockito.MockitoSugar
import cats.implicits._
import vinyldns.api.Interfaces._
-import vinyldns.api.ResultHelpers
import cats.effect._
import org.scalatest.{BeforeAndAfterEach, EitherValues}
+import vinyldns.api.config.ValidEmailConfig
import vinyldns.api.domain.access.AccessValidations
+import vinyldns.api.domain.membership.{EmailValidationError, MembershipService}
+import vinyldns.core.domain.record.RecordSetRepository
+//import vinyldns.api.domain.membership.{EmailValidationError, MembershipService}
import vinyldns.api.repository.TestDataLoader
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.membership._
@@ -35,15 +38,13 @@ import vinyldns.core.queue.MessageQueue
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData._
import vinyldns.core.crypto.NoOpCrypto
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.backend.BackendResolver
-import scala.concurrent.duration._
-
class ZoneServiceSpec
extends AnyWordSpec
with Matchers
with MockitoSugar
- with ResultHelpers
with BeforeAndAfterEach
with EitherValues {
@@ -53,9 +54,27 @@ class ZoneServiceSpec
private val mockZoneChangeRepo = mock[ZoneChangeRepository]
private val mockMessageQueue = mock[MessageQueue]
private val mockBackendResolver = mock[BackendResolver]
- private val badConnection = ZoneConnection("bad", "bad", "bad", "bad")
+ private val badConnection = ZoneConnection("bad", "bad", Encrypted("bad"), "bad")
private val abcZoneSummary = ZoneSummaryInfo(abcZone, abcGroup.name, AccessLevel.Delete)
private val xyzZoneSummary = ZoneSummaryInfo(xyzZone, xyzGroup.name, AccessLevel.NoAccess)
+ private val abcDeletedZoneSummary = ZoneChangeDeletedInfo(abcDeletedZoneChange, abcGroup.name, okUser.userName, AccessLevel.Delete)
+ private val xyzDeletedZoneSummary = ZoneChangeDeletedInfo(xyzDeletedZoneChange, xyzGroup.name, okUser.userName, AccessLevel.NoAccess)
+ private val zoneIp4ZoneSummary = ZoneSummaryInfo(zoneIp4, abcGroup.name, AccessLevel.Delete)
+ private val zoneIp6ZoneSummary = ZoneSummaryInfo(zoneIp6, abcGroup.name, AccessLevel.Delete)
+ private val mockMembershipRepo = mock[MembershipRepository]
+ private val mockGroupChangeRepo = mock[GroupChangeRepository]
+ private val mockRecordSetRepo = mock[RecordSetRepository]
+ private val mockValidEmailConfig = ValidEmailConfig(valid_domains = List("test.com", "*dummy.com"),2)
+ private val mockValidEmailConfigNew = ValidEmailConfig(valid_domains = List(),2)
+ private val mockMembershipService = new MembershipService(mockGroupRepo,
+ mockUserRepo,
+ mockMembershipRepo,
+ mockZoneRepo,
+ mockGroupChangeRepo,
+ mockRecordSetRepo,
+ mockValidEmailConfig)
+
+
object TestConnectionValidator extends ZoneConnectionValidatorAlgebra {
def validateZoneConnections(zone: Zone): Result[Unit] =
@@ -81,7 +100,27 @@ class ZoneServiceSpec
new ZoneValidations(1000),
new AccessValidations(),
mockBackendResolver,
- NoOpCrypto.instance
+ NoOpCrypto.instance,
+ mockMembershipService
+ )
+ private val underTestNew = new ZoneService(
+ mockZoneRepo,
+ mockGroupRepo,
+ mockUserRepo,
+ mockZoneChangeRepo,
+ TestConnectionValidator,
+ mockMessageQueue,
+ new ZoneValidations(1000),
+ new AccessValidations(),
+ mockBackendResolver,
+ NoOpCrypto.instance,
+ new MembershipService(mockGroupRepo,
+ mockUserRepo,
+ mockMembershipRepo,
+ mockZoneRepo,
+ mockGroupChangeRepo,
+ mockRecordSetRepo,
+ mockValidEmailConfigNew)
)
private val createZoneAuthorized = CreateZoneInput(
@@ -94,7 +133,7 @@ class ZoneServiceSpec
private val updateZoneAuthorized = UpdateZoneInput(
okZone.id,
"ok.zone.recordsets.",
- "updated-test@test.com",
+ "test@test.com",
connection = testConnection,
adminGroupId = okGroup.id
)
@@ -109,9 +148,8 @@ class ZoneServiceSpec
"return an appropriate zone change response" in {
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
- val resultChange: ZoneChange = rightResultOf(
- underTest.connectToZone(createZoneAuthorized, okAuth).map(_.asInstanceOf[ZoneChange]).value
- )
+ val resultChange: ZoneChange =
+ underTest.connectToZone(createZoneAuthorized, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
resultChange.changeType shouldBe ZoneChangeType.Create
Option(resultChange.created) shouldBe defined
@@ -131,12 +169,11 @@ class ZoneServiceSpec
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
val nonTestUser = okAuth.copy(signedInUser = okAuth.signedInUser.copy(isTest = false))
- val resultChange: ZoneChange = rightResultOf(
+ val resultChange: ZoneChange =
underTest
.connectToZone(createZoneAuthorized, nonTestUser)
.map(_.asInstanceOf[ZoneChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
resultChange.zone.isTest shouldBe false
}
@@ -146,12 +183,11 @@ class ZoneServiceSpec
val testUser = okAuth.copy(signedInUser = okAuth.signedInUser.copy(isTest = true))
testUser.isTestUser shouldBe true
- val resultChange: ZoneChange = rightResultOf(
+ val resultChange: ZoneChange =
underTest
.connectToZone(createZoneAuthorized, testUser)
.map(_.asInstanceOf[ZoneChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
resultChange.zone.isTest shouldBe true
}
@@ -159,7 +195,7 @@ class ZoneServiceSpec
"return a ZoneAlreadyExists error if the zone exists" in {
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
- val error = leftResultOf(underTest.connectToZone(createZoneAuthorized, okAuth).value)
+ val error = underTest.connectToZone(createZoneAuthorized, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[ZoneAlreadyExistsError]
}
@@ -168,7 +204,7 @@ class ZoneServiceSpec
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(anyString)
- val error = leftResultOf(underTest.connectToZone(createZoneAuthorized, okAuth).value)
+ val error = underTest.connectToZone(createZoneAuthorized, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidGroupError]
}
@@ -176,27 +212,175 @@ class ZoneServiceSpec
"allow the zone to be created if it exists and the zone is deleted" in {
doReturn(IO.pure(Some(zoneDeleted))).when(mockZoneRepo).getZoneByName(anyString)
- val resultChange: ZoneChange = rightResultOf(
- underTest.connectToZone(createZoneAuthorized, okAuth).map(_.asInstanceOf[ZoneChange]).value
- )
+ val resultChange: ZoneChange =
+ underTest.connectToZone(createZoneAuthorized, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
+
resultChange.changeType shouldBe ZoneChangeType.Create
}
+ "return a NotAuthorizedError when zone recurrence schedule is set by a non-superuser" in {
+ doReturn(IO.pure(Some(zoneDeleted))).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+
+ error shouldBe an[NotAuthorizedError]
+ }
+
+ "allow the zone to be created when zone recurrence schedule is set by a superuser" in {
+ doReturn(IO.pure(Some(zoneDeleted))).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ val resultChange: ZoneChange = underTest.connectToZone(newZone, superUserAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
+
+ resultChange.changeType shouldBe ZoneChangeType.Create
+ resultChange.zone.recurrenceSchedule shouldBe Some("0/5 0 0 ? * * *")
+ }
+
+ "return a InvalidRequest when zone recurrence schedule cron expression is invalid" in {
+ doReturn(IO.pure(Some(zoneDeleted))).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(recurrenceSchedule = Some("abcd"))
+ val error = underTest.connectToZone(newZone, superUserAuth).value.unsafeRunSync().swap.toOption.get
+
+ error shouldBe an[InvalidRequest]
+ }
+
"return an error if the zone create includes a bad acl rule" in {
val badAcl = ACLRule(baseAclRuleInfo.copy(recordMask = Some("x{5,-3}")))
val newZone = createZoneAuthorized.copy(acl = ZoneACL(Set(badAcl)))
- val error = leftResultOf(underTest.connectToZone(newZone, okAuth).value)
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidRequest]
}
+ "return the result if the zone created includes an valid email" in {
+ doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(email ="test@ok.dummy.com")
+ val resultChange: ZoneChange =
+ underTest.connectToZone(newZone, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
+ resultChange.changeType shouldBe ZoneChangeType.Create
+ Option(resultChange.created) shouldBe defined
+ resultChange.status shouldBe ZoneChangeStatus.Pending
+ resultChange.userId shouldBe okAuth.userId
+
+ val resultZone = resultChange.zone
+ Option(resultZone.id) shouldBe defined
+ resultZone.email shouldBe newZone.email
+ resultZone.name shouldBe newZone.name
+ resultZone.status shouldBe ZoneStatus.Syncing
+ resultZone.connection shouldBe newZone.connection
+ resultZone.shared shouldBe false
+ }
+
+ "return the result if the zone created includes an valid email with number of dots " in {
+ doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(email = "test@ok.dummy.com")
+ val resultChange: ZoneChange =
+ underTest.connectToZone(newZone, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
+ resultChange.changeType shouldBe ZoneChangeType.Create
+ Option(resultChange.created) shouldBe defined
+ resultChange.status shouldBe ZoneChangeStatus.Pending
+ resultChange.userId shouldBe okAuth.userId
+
+ val resultZone = resultChange.zone
+ Option(resultZone.id) shouldBe defined
+ resultZone.email shouldBe newZone.email
+ resultZone.name shouldBe newZone.name
+ resultZone.status shouldBe ZoneStatus.Syncing
+ resultZone.connection shouldBe newZone.connection
+ resultZone.shared shouldBe false
+ }
+
+ "return the result if the zone created includes empty Domain" in {
+ doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
+
+ val newZone = createZoneAuthorized.copy(email = "test@abc.com")
+ val resultChange: ZoneChange =
+ underTestNew.connectToZone(newZone, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
+ resultChange.changeType shouldBe ZoneChangeType.Create
+ Option(resultChange.created) shouldBe defined
+ resultChange.status shouldBe ZoneChangeStatus.Pending
+ resultChange.userId shouldBe okAuth.userId
+
+ val resultZone = resultChange.zone
+ Option(resultZone.id) shouldBe defined
+ resultZone.email shouldBe newZone.email
+ resultZone.name shouldBe newZone.name
+ resultZone.status shouldBe ZoneStatus.Syncing
+ resultZone.connection shouldBe newZone.connection
+ resultZone.shared shouldBe false
+ }
+ "return an EmailValidationError if an email is invalid" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test@my.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case with number of dots" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test@ok.ok.dummy.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 1" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test.ok.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if a domain is invalid test case 1" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test@ok.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 2" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test@.@.test.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 3" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "test@.@@.test.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 4" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "@te@st@test.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 5" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = ".test@test.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 6" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZoneByName(anyString)
+ val newZone = createZoneAuthorized.copy(email = "te.....st@test.com")
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
"succeed if zone is shared and user is a super user" in {
val newZone = createZoneAuthorized.copy(shared = true)
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
- val resultZone = rightResultOf(
- underTest.connectToZone(newZone, superUserAuth).map(_.asInstanceOf[ZoneChange]).value
- ).zone
+ val resultZone =
+ underTest.connectToZone(newZone, superUserAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get.zone
Option(resultZone.id) should not be None
resultZone.email shouldBe okZone.email
@@ -210,12 +394,11 @@ class ZoneServiceSpec
val newZone = createZoneAuthorized.copy(shared = true)
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
- val resultZone = rightResultOf(
+ val resultZone =
underTest
.connectToZone(newZone, supportUserAuth)
.map(_.asInstanceOf[ZoneChange])
- .value
- ).zone
+ .value.unsafeRunSync().toOption.get.zone
Option(resultZone.id) should not be None
resultZone.email shouldBe okZone.email
@@ -229,14 +412,14 @@ class ZoneServiceSpec
val newZone = createZoneAuthorized.copy(shared = true)
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName(anyString)
- val error = leftResultOf(underTest.connectToZone(newZone, okAuth).value)
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
"return an InvalidRequest if zone has a specified backend ID that is invalid" in {
val newZone = createZoneAuthorized.copy(backendId = Some("badId"))
- val error = leftResultOf(underTest.connectToZone(newZone, okAuth).value)
+ val error = underTest.connectToZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidRequest]
}
}
@@ -248,13 +431,11 @@ class ZoneServiceSpec
val doubleAuth = AuthPrincipal(TestDataLoader.okUser, Seq(twoUserGroup.id, okGroup.id))
val updateZoneInput = updateZoneAuthorized.copy(adminGroupId = twoUserGroup.id)
- val resultChange: ZoneChange = rightResultOf(
+ val resultChange: ZoneChange =
underTest
.updateZone(updateZoneInput, doubleAuth)
.map(_.asInstanceOf[ZoneChange])
- .value,
- duration = 2.seconds
- )
+ .value.unsafeRunSync().toOption.get
resultChange.zone.id shouldBe okZone.id
resultChange.changeType shouldBe ZoneChangeType.Update
@@ -262,6 +443,42 @@ class ZoneServiceSpec
resultChange.zone.adminGroupId should not be updateZoneAuthorized.adminGroupId
}
+ "return a NotAuthorizedError when zone recurrence schedule is updated by a non-superuser" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val doubleAuth = AuthPrincipal(TestDataLoader.okUser, Seq(twoUserGroup.id, okGroup.id))
+ val updateZoneInput = updateZoneAuthorized.copy(recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ val error = underTest
+ .updateZone(updateZoneInput, doubleAuth).value.unsafeRunSync().swap.toOption.get
+
+ error shouldBe an[NotAuthorizedError]
+ }
+
+ "return a InvalidRequest when zone recurrence schedule cron expression is invalid" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val updateZoneInput = updateZoneAuthorized.copy(recurrenceSchedule = Some("abcd"))
+ val error = underTest
+ .updateZone(updateZoneInput, superUserAuth).value.unsafeRunSync().swap.toOption.get
+
+ error shouldBe an[InvalidRequest]
+ }
+
+ "allow the zone to be created when zone recurrence schedule is set by a superuser" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val updateZoneInput = updateZoneAuthorized.copy(recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ val resultChange: ZoneChange =
+ underTest
+ .updateZone(updateZoneInput, superUserAuth)
+ .map(_.asInstanceOf[ZoneChange])
+ .value.unsafeRunSync().toOption.get
+
+ resultChange.zone.id shouldBe okZone.id
+ resultChange.changeType shouldBe ZoneChangeType.Update
+ resultChange.zone.recurrenceSchedule shouldBe updateZoneInput.recurrenceSchedule
+ }
+
"not validate connection if unchanged" in {
val oldZone = okZone.copy(connection = Some(badConnection))
doReturn(IO.pure(Some(oldZone))).when(mockZoneRepo).getZone(anyString)
@@ -272,12 +489,11 @@ class ZoneServiceSpec
val doubleAuth = AuthPrincipal(TestDataLoader.okUser, Seq(okGroup.id, okGroup.id))
val resultChange: ZoneChange =
- rightResultOf(
underTest
.updateZone(newZone, doubleAuth)
.map(_.asInstanceOf[ZoneChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
resultChange.zone.id shouldBe oldZone.id
resultChange.zone.connection shouldBe oldZone.connection
}
@@ -288,7 +504,7 @@ class ZoneServiceSpec
val newZone =
updateZoneAuthorized.copy(connection = Some(badConnection), adminGroupId = okGroup.id)
- val error = leftResultOf(underTest.updateZone(newZone, okAuth).value)
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[ConnectionFailed]
}
@@ -297,7 +513,7 @@ class ZoneServiceSpec
val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
- val error = leftResultOf(underTest.updateZone(updateZoneAuthorized, noAuth).value)
+ val error = underTest.updateZone(updateZoneAuthorized, noAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -307,9 +523,43 @@ class ZoneServiceSpec
val badAcl = ACLRule(baseAclRuleInfo.copy(recordMask = Some("x{5,-3}")))
val newZone = updateZoneAuthorized.copy(acl = ZoneACL(Set(badAcl)))
- val error = leftResultOf(underTest.updateZone(newZone, okAuth).value)
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidRequest]
}
+ "return the result if the zone updated includes an valid email" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val doubleAuth = AuthPrincipal(TestDataLoader.okUser, Seq(twoUserGroup.id, okGroup.id))
+ val updateZoneInput = updateZoneAuthorized.copy(adminGroupId = twoUserGroup.id,email="test@dummy.com")
+
+ val resultChange: ZoneChange =
+ underTest
+ .updateZone(updateZoneInput, doubleAuth)
+ .map(_.asInstanceOf[ZoneChange])
+ .value.unsafeRunSync().toOption.get
+
+ resultChange.zone.id shouldBe okZone.id
+ resultChange.changeType shouldBe ZoneChangeType.Update
+ resultChange.zone.adminGroupId shouldBe updateZoneInput.adminGroupId
+ resultChange.zone.adminGroupId should not be updateZoneAuthorized.adminGroupId
+ }
+ "return the result if the zone updated includes an empty domain" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val doubleAuth = AuthPrincipal(TestDataLoader.okUser, Seq(twoUserGroup.id, okGroup.id))
+ val updateZoneInput = updateZoneAuthorized.copy(adminGroupId = twoUserGroup.id, email = "test@ok.com")
+
+ val resultChange: ZoneChange =
+ underTestNew
+ .updateZone(updateZoneInput, doubleAuth)
+ .map(_.asInstanceOf[ZoneChange])
+ .value.unsafeRunSync().toOption.get
+
+ resultChange.zone.id shouldBe okZone.id
+ resultChange.changeType shouldBe ZoneChangeType.Update
+ resultChange.zone.adminGroupId shouldBe updateZoneInput.adminGroupId
+ resultChange.zone.adminGroupId should not be updateZoneAuthorized.adminGroupId
+ }
"succeed if zone shared flag is updated and user is a super user" in {
val newZone = updateZoneAuthorized.copy(shared = false)
@@ -317,11 +567,11 @@ class ZoneServiceSpec
.when(mockZoneRepo)
.getZone(newZone.id)
- val result = rightResultOf(
+ val result =
underTest
.updateZone(newZone, AuthPrincipal(superUser, List.empty))
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe a[ZoneChange]
}
@@ -331,11 +581,11 @@ class ZoneServiceSpec
.when(mockZoneRepo)
.getZone(newZone.id)
- val result = rightResultOf(
+ val result =
underTest
.updateZone(newZone, supportUserAuth)
- .value
- )
+ .value.unsafeRunSync().toOption.get
+
result shouldBe a[ZoneChange]
}
@@ -346,7 +596,7 @@ class ZoneServiceSpec
.when(mockZoneRepo)
.getZone(newZone.id)
- val error = leftResultOf(underTest.updateZone(newZone, okAuth).value)
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -356,15 +606,55 @@ class ZoneServiceSpec
.when(mockZoneRepo)
.getZone(newZone.id)
- val result = rightResultOf(underTest.updateZone(newZone, okAuth).value)
+ val result = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().toOption.get
result shouldBe a[ZoneChange]
}
"return an InvalidRequest if zone has a specified backend ID that is invalid" in {
val newZone = updateZoneAuthorized.copy(backendId = Some("badId"))
- val error = leftResultOf(underTest.updateZone(newZone, okAuth).value)
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidRequest]
}
+ "return an EmailValidationError if an invalid email is entered while updating the zone" in {
+ val newZone = updateZoneAuthorized.copy(email ="test.ok.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe an[EmailValidationError]
+ }
+ "return an EmailValidationError if a domain is invalid test case 1" in {
+ val newZone = updateZoneAuthorized.copy(email = "test@ok.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe an[EmailValidationError]
+ }
+
+ "return an EmailValidationError if an email is invalid test case 2" in {
+ val newZone = updateZoneAuthorized.copy(email = "test@.@.test.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 3" in {
+ val newZone = updateZoneAuthorized.copy(email = "test@.@@.test.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 4" in {
+ val newZone=updateZoneAuthorized.copy(email = "@te@st@test.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 5" in {
+ val newZone = updateZoneAuthorized.copy(email = ".test@test.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
+
+ "return an error if an email is invalid test case 6" in {
+ val newZone = updateZoneAuthorized.copy(email = "te.....st@test.com")
+ val error = underTest.updateZone(newZone, okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[EmailValidationError]
+ }
}
"Deleting Zones" should {
@@ -372,7 +662,7 @@ class ZoneServiceSpec
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
val resultChange: ZoneChange =
- rightResultOf(underTest.deleteZone(okZone.id, okAuth).map(_.asInstanceOf[ZoneChange]).value)
+ underTest.deleteZone(okZone.id, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
resultChange.zone.id shouldBe okZone.id
resultChange.changeType shouldBe ZoneChangeType.Delete
@@ -383,7 +673,7 @@ class ZoneServiceSpec
val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
- val error = leftResultOf(underTest.deleteZone(okZone.id, noAuth).value)
+ val error = underTest.deleteZone(okZone.id, noAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
}
@@ -393,7 +683,7 @@ class ZoneServiceSpec
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
val resultChange: ZoneChange =
- rightResultOf(underTest.syncZone(okZone.id, okAuth).map(_.asInstanceOf[ZoneChange]).value)
+ underTest.syncZone(okZone.id, okAuth).map(_.asInstanceOf[ZoneChange]).value.unsafeRunSync().toOption.get
resultChange.zone.id shouldBe okZone.id
resultChange.changeType shouldBe ZoneChangeType.Sync
@@ -405,7 +695,7 @@ class ZoneServiceSpec
val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
- val error = leftResultOf(underTest.syncZone(okZone.id, noAuth).value)
+ val error = underTest.syncZone(okZone.id, noAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
}
@@ -414,7 +704,7 @@ class ZoneServiceSpec
"fail with no zone returned" in {
doReturn(IO.pure(None)).when(mockZoneRepo).getZone("notAZoneId")
- val error = leftResultOf(underTest.getZone("notAZoneId", okAuth).value)
+ val error = underTest.getZone("notAZoneId", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[ZoneNotFoundError]
}
@@ -423,7 +713,7 @@ class ZoneServiceSpec
val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
- val error = leftResultOf(underTest.getZone(okZone.id, noAuth).value)
+ val error = underTest.getZone(okZone.id, noAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -442,7 +732,7 @@ class ZoneServiceSpec
}
"filter out ACL rules that have no matching group or user" in {
- val goodUser = User("goodUser", "access", "secret")
+ val goodUser = User("goodUser", "access", Encrypted("secret"))
val goodGroup = Group("goodGroup", "email")
val goodUserRule = baseAclRule.copy(userId = Some(goodUser.id), groupId = None)
@@ -471,7 +761,7 @@ class ZoneServiceSpec
goodGroup.name,
AccessLevel.Delete
)
- val result: ZoneInfo = rightResultOf(underTest.getZone(zoneWithRules.id, abcAuth).value)
+ val result: ZoneInfo = underTest.getZone(zoneWithRules.id, abcAuth).value.unsafeRunSync().toOption.get
result shouldBe expectedZoneInfo
}
@@ -485,14 +775,14 @@ class ZoneServiceSpec
val expectedZoneInfo =
ZoneInfo(abcZone, ZoneACLInfo(Set()), "Unknown group name", AccessLevel.Delete)
- val result: ZoneInfo = rightResultOf(underTest.getZone(abcZone.id, abcAuth).value)
+ val result: ZoneInfo = underTest.getZone(abcZone.id, abcAuth).value.unsafeRunSync().toOption.get
result shouldBe expectedZoneInfo
}
"return a zone by name with failure when no zone is found" in {
doReturn(IO.pure(None)).when(mockZoneRepo).getZoneByName("someZoneName.")
- val error = leftResultOf(underTest.getZoneByName("someZoneName", okAuth).value)
+ val error = underTest.getZoneByName("someZoneName", okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[ZoneNotFoundError]
}
@@ -511,14 +801,54 @@ class ZoneServiceSpec
}
}
+ "Getting a zone details" should {
+ "fail with no zone returned" in {
+ doReturn(IO.pure(None)).when(mockZoneRepo).getZone("notAZoneId")
+
+ val error = underTest.getCommonZoneDetails("notAZoneId", okAuth).value.unsafeRunSync().swap.toOption.get
+ error shouldBe a[ZoneNotFoundError]
+ }
+
+ "return zone details even if the user is not authorized for the zone" in {
+ doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
+
+ val noAuth = AuthPrincipal(TestDataLoader.okUser, Seq())
+
+ val result = underTest.getCommonZoneDetails(okZone.id, noAuth).value.unsafeRunSync()
+ val expectedZoneDetails =
+ ZoneDetails(okZone, okGroup.name)
+ result.right.value shouldBe expectedZoneDetails
+ }
+
+ "return the appropriate zone as a ZoneDetails" in {
+ doReturn(IO.pure(Some(abcZone))).when(mockZoneRepo).getZone(abcZone.id)
+ doReturn(IO.pure(Some(abcGroup))).when(mockGroupRepo).getGroup(anyString)
+
+ val expectedZoneDetails =
+ ZoneDetails(abcZone, abcGroup.name)
+ val result = underTest.getCommonZoneDetails(abcZone.id, abcAuth).value.unsafeRunSync()
+ result.right.value shouldBe expectedZoneDetails
+ }
+
+ "return Unknown group name if zone admin group cannot be found" in {
+ doReturn(IO.pure(Some(abcZone))).when(mockZoneRepo).getZone(abcZone.id)
+ doReturn(IO.pure(None)).when(mockGroupRepo).getGroup(anyString)
+
+ val expectedZoneDetails =
+ ZoneDetails(abcZone, "Unknown group name")
+ val result: ZoneDetails = underTest.getCommonZoneDetails(abcZone.id, abcAuth).value.unsafeRunSync().toOption.get
+ result shouldBe expectedZoneDetails
+ }
+ }
+
"ListZones" should {
"not fail with no zones returned" in {
doReturn(IO.pure(ListZonesResults(List())))
.when(mockZoneRepo)
- .listZones(abcAuth, None, None, 100, false)
+ .listZones(abcAuth, None, None, 100, false, true)
doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
- val result: ListZonesResponse = rightResultOf(underTest.listZones(abcAuth).value)
+ val result: ListZonesResponse = underTest.listZones(abcAuth).value.unsafeRunSync().toOption.get
result.zones shouldBe List()
result.maxItems shouldBe 100
result.startFrom shouldBe None
@@ -530,12 +860,12 @@ class ZoneServiceSpec
"return the appropriate zones" in {
doReturn(IO.pure(ListZonesResults(List(abcZone))))
.when(mockZoneRepo)
- .listZones(abcAuth, None, None, 100, false)
+ .listZones(abcAuth, None, None, 100, false, true)
doReturn(IO.pure(Set(abcGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
- val result: ListZonesResponse = rightResultOf(underTest.listZones(abcAuth).value)
+ val result: ListZonesResponse = underTest.listZones(abcAuth).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
@@ -545,41 +875,84 @@ class ZoneServiceSpec
}
"return all zones" in {
- doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone), ignoreAccess = true)))
+ doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone, zoneIp4, zoneIp6), ignoreAccess = true, includeReverse = true)))
.when(mockZoneRepo)
- .listZones(abcAuth, None, None, 100, true)
+ .listZones(abcAuth, None, None, 100, true, true)
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, ignoreAccess = true).value)
+ underTest.listZones(abcAuth, ignoreAccess = true, includeReverse = true).value.unsafeRunSync().toOption.get
+ result.zones shouldBe List(abcZoneSummary, xyzZoneSummary, zoneIp4ZoneSummary, zoneIp6ZoneSummary)
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.nameFilter shouldBe None
+ result.nextId shouldBe None
+ result.ignoreAccess shouldBe true
+ result.includeReverse shouldBe true
+ }
+
+ "return all forward zones" in {
+ doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone), ignoreAccess = true, includeReverse = false)))
+ .when(mockZoneRepo)
+ .listZones(abcAuth, None, None, 100, true, false)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+
+ val result: ListZonesResponse =
+ underTest.listZones(abcAuth, ignoreAccess = true, includeReverse = false).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary, xyzZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
result.nameFilter shouldBe None
result.nextId shouldBe None
result.ignoreAccess shouldBe true
+ result.includeReverse shouldBe false
}
"name filter must be used to return zones by admin group name, when search by admin group option is true" in {
doReturn(IO.pure(Set(abcGroup)))
.when(mockGroupRepo)
.getGroupsByName(any[String])
- doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcGroup"))))
+ doReturn(IO.pure(ListZonesResults(List(abcZone, zoneIp4, zoneIp6), ignoreAccess = true, zonesFilter = Some("abcGroup"))))
.when(mockZoneRepo)
- .listZonesByAdminGroupIds(abcAuth, None, 100, Set(abcGroup.id), ignoreAccess = true)
+ .listZonesByAdminGroupIds(abcAuth, None, 100, Set(abcGroup.id), ignoreAccess = true, includeReverse = true)
doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
// When searchByAdminGroup is true, zones are filtered by admin group name given in nameFilter
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, Some("abcGroup"), None, 100, searchByAdminGroup = true, ignoreAccess = true).value)
+ underTest.listZones(abcAuth, Some("abcGroup"), None, 100, searchByAdminGroup = true, ignoreAccess = true).value.unsafeRunSync().toOption.get
+ result.zones shouldBe List(abcZoneSummary, zoneIp4ZoneSummary, zoneIp6ZoneSummary)
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.nameFilter shouldBe Some("abcGroup")
+ result.nextId shouldBe None
+ result.ignoreAccess shouldBe true
+ result.includeReverse shouldBe true
+ }
+
+ "name filter must be used to return forward zones by admin group name, when search by admin group option is true and includeReverse is false" in {
+ doReturn(IO.pure(Set(abcGroup)))
+ .when(mockGroupRepo)
+ .getGroupsByName(any[String])
+ doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcGroup"), includeReverse = false)))
+ .when(mockZoneRepo)
+ .listZonesByAdminGroupIds(abcAuth, None, 100, Set(abcGroup.id), ignoreAccess = true, includeReverse = false)
+ doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
+
+ // When searchByAdminGroup is true, zones are filtered by admin group name given in nameFilter.
+ // Reverse zones are excluded when includeReverse is false.
+ val result: ListZonesResponse =
+ underTest.listZones(abcAuth, Some("abcGroup"), None, 100, searchByAdminGroup = true, ignoreAccess = true, includeReverse = false).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
result.nameFilter shouldBe Some("abcGroup")
result.nextId shouldBe None
result.ignoreAccess shouldBe true
+ result.includeReverse shouldBe false
}
"name filter must be used to return zone by zone name, when search by admin group option is false" in {
@@ -588,11 +961,11 @@ class ZoneServiceSpec
.getGroups(any[Set[String]])
doReturn(IO.pure(ListZonesResults(List(abcZone), ignoreAccess = true, zonesFilter = Some("abcZone"))))
.when(mockZoneRepo)
- .listZones(abcAuth, Some("abcZone"), None, 100, true)
+ .listZones(abcAuth, Some("abcZone"), None, 100, true, true)
// When searchByAdminGroup is false, zone name given in nameFilter is returned
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, Some("abcZone"), None, 100, searchByAdminGroup = false, ignoreAccess = true).value)
+ underTest.listZones(abcAuth, Some("abcZone"), None, 100, searchByAdminGroup = false, ignoreAccess = true).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary)
result.maxItems shouldBe 100
result.startFrom shouldBe None
@@ -604,10 +977,10 @@ class ZoneServiceSpec
"return Unknown group name if zone admin group cannot be found" in {
doReturn(IO.pure(ListZonesResults(List(abcZone, xyzZone))))
.when(mockZoneRepo)
- .listZones(abcAuth, None, None, 100, false)
+ .listZones(abcAuth, None, None, 100, false, true)
doReturn(IO.pure(Set(okGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
- val result: ListZonesResponse = rightResultOf(underTest.listZones(abcAuth).value)
+ val result: ListZonesResponse = underTest.listZones(abcAuth).value.unsafeRunSync().toOption.get
val expectedZones =
List(abcZoneSummary, xyzZoneSummary).map(_.copy(adminGroupName = "Unknown group name"))
result.zones shouldBe expectedZones
@@ -628,13 +1001,13 @@ class ZoneServiceSpec
)
)
).when(mockZoneRepo)
- .listZones(abcAuth, None, None, 2, false)
+ .listZones(abcAuth, None, None, 2, false, true)
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, maxItems = 2).value)
+ underTest.listZones(abcAuth, maxItems = 2).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary, xyzZoneSummary)
result.maxItems shouldBe 2
result.startFrom shouldBe None
@@ -654,13 +1027,13 @@ class ZoneServiceSpec
)
)
).when(mockZoneRepo)
- .listZones(abcAuth, Some("foo"), None, 2, false)
+ .listZones(abcAuth, Some("foo"), None, 2, false, true)
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value)
+ underTest.listZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary, xyzZoneSummary)
result.nameFilter shouldBe Some("foo")
result.nextId shouldBe Some("zone2.")
@@ -678,13 +1051,13 @@ class ZoneServiceSpec
)
)
).when(mockZoneRepo)
- .listZones(abcAuth, None, Some("zone4."), 2, false)
+ .listZones(abcAuth, None, Some("zone4."), 2, false, true)
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value)
+ underTest.listZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary, xyzZoneSummary)
result.startFrom shouldBe Some("zone4.")
}
@@ -701,18 +1074,207 @@ class ZoneServiceSpec
)
)
).when(mockZoneRepo)
- .listZones(abcAuth, None, Some("zone4."), 2, false)
+ .listZones(abcAuth, None, Some("zone4."), 2, false, true)
doReturn(IO.pure(Set(abcGroup, xyzGroup)))
.when(mockGroupRepo)
.getGroups(any[Set[String]])
val result: ListZonesResponse =
- rightResultOf(underTest.listZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value)
+ underTest.listZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
result.zones shouldBe List(abcZoneSummary, xyzZoneSummary)
result.nextId shouldBe Some("zone6.")
}
}
+ "ListDeletedZones" should {
+ "not fail with no zones returned" in {
+
+ doReturn(IO.pure(ListDeletedZonesChangeResults(List())))
+ .when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, None, 100, false)
+ doReturn(IO.pure(Set(abcGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List()
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.zoneChangeFilter shouldBe None
+ result.nextId shouldBe None
+ result.ignoreAccess shouldBe false
+ }
+
+ "return the appropriate zones" in {
+ doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange))))
+ .when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, None, 100, false)
+ doReturn(IO.pure(Set(abcGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
+
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary)
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.zoneChangeFilter shouldBe None
+ result.nextId shouldBe None
+ result.ignoreAccess shouldBe false
+ }
+
+ "return all zones" in {
+ doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange), ignoreAccess = true)))
+ .when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, None, 100, true)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse =
+ underTest.listDeletedZones(abcAuth, ignoreAccess = true).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary,xyzDeletedZoneSummary)
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.zoneChangeFilter shouldBe None
+ result.nextId shouldBe None
+ result.ignoreAccess shouldBe true
+ }
+
+ "return Unknown group name if zone admin group cannot be found" in {
+ doReturn(IO.pure(ListDeletedZonesChangeResults(List(abcDeletedZoneChange, xyzDeletedZoneChange))))
+ .when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, None, 100, false)
+ doReturn(IO.pure(Set(okGroup))).when(mockGroupRepo).getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse = underTest.listDeletedZones(abcAuth).value.unsafeRunSync().toOption.get
+ val expectedZones =
+ List(abcDeletedZoneSummary, xyzDeletedZoneSummary).map(_.copy(adminGroupName = "Unknown group name"))
+ result.zonesDeletedInfo shouldBe expectedZones
+ result.maxItems shouldBe 100
+ result.startFrom shouldBe None
+ result.zoneChangeFilter shouldBe None
+ result.nextId shouldBe None
+ }
+
+ "set the nextId appropriately" in {
+ doReturn(
+ IO.pure(
+ ListDeletedZonesChangeResults(
+ List(abcDeletedZoneChange, xyzDeletedZoneChange),
+ maxItems = 2,
+ nextId = Some("zone2."),
+ ignoreAccess = false
+ )
+ )
+ ).when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, None, 2, false)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse =
+ underTest.listDeletedZones(abcAuth, maxItems = 2).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
+ result.maxItems shouldBe 2
+ result.startFrom shouldBe None
+ result.zoneChangeFilter shouldBe None
+ result.nextId shouldBe Some("zone2.")
+ }
+
+ "set the nameFilter when provided" in {
+ doReturn(
+ IO.pure(
+ ListDeletedZonesChangeResults(
+ List(abcDeletedZoneChange, xyzDeletedZoneChange),
+ zoneChangeFilter = Some("foo"),
+ maxItems = 2,
+ nextId = Some("zone2."),
+ ignoreAccess = false
+ )
+ )
+ ).when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, Some("foo"), None, 2, false)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse =
+ underTest.listDeletedZones(abcAuth, nameFilter = Some("foo"), maxItems = 2).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
+ result.zoneChangeFilter shouldBe Some("foo")
+ result.nextId shouldBe Some("zone2.")
+ result.maxItems shouldBe 2
+ }
+
+ "set the startFrom when provided" in {
+ doReturn(
+ IO.pure(
+ ListDeletedZonesChangeResults(
+ List(abcDeletedZoneChange, xyzDeletedZoneChange),
+ startFrom = Some("zone4."),
+ maxItems = 2,
+ ignoreAccess = false
+ )
+ )
+ ).when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, Some("zone4."), 2, false)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse =
+ underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
+ result.startFrom shouldBe Some("zone4.")
+ }
+
+ "set the nextId to be the current result set size plus the start from" in {
+ doReturn(
+ IO.pure(
+ ListDeletedZonesChangeResults(
+ List(abcDeletedZoneChange, xyzDeletedZoneChange),
+ startFrom = Some("zone4."),
+ maxItems = 2,
+ nextId = Some("zone6."),
+ ignoreAccess = false
+ )
+ )
+ ).when(mockZoneChangeRepo)
+ .listDeletedZones(abcAuth, None, Some("zone4."), 2, false)
+ doReturn(IO.pure(Set(abcGroup, xyzGroup)))
+ .when(mockGroupRepo)
+ .getGroups(any[Set[String]])
+ doReturn(IO.pure(ListUsersResults(Seq(okUser), None)))
+ .when(mockUserRepo)
+ .getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
+
+ val result: ListDeletedZoneChangesResponse =
+ underTest.listDeletedZones(abcAuth, startFrom = Some("zone4."), maxItems = 2).value.unsafeRunSync().toOption.get
+ result.zonesDeletedInfo shouldBe List(abcDeletedZoneSummary, xyzDeletedZoneSummary)
+ result.nextId shouldBe Some("zone6.")
+ }
+ }
+
"listZoneChanges" should {
"retrieve the zone changes" in {
doReturn(IO.pure(Some(okZone)))
@@ -723,7 +1285,7 @@ class ZoneServiceSpec
.listZoneChanges(okZone.id, startFrom = None, maxItems = 100)
val result: ListZoneChangesResponse =
- rightResultOf(underTest.listZoneChanges(okZone.id, okAuth).value)
+ underTest.listZoneChanges(okZone.id, okAuth).value.unsafeRunSync().toOption.get
result.zoneChanges shouldBe List(zoneUpdate, zoneCreate)
result.zoneId shouldBe okZone.id
@@ -738,7 +1300,7 @@ class ZoneServiceSpec
.listZoneChanges(okZone.id, startFrom = None, maxItems = 100)
val result: ListZoneChangesResponse =
- rightResultOf(underTest.listZoneChanges(okZone.id, okAuth).value)
+ underTest.listZoneChanges(okZone.id, okAuth).value.unsafeRunSync().toOption.get
result.zoneChanges shouldBe empty
result.zoneId shouldBe okZone.id
@@ -749,7 +1311,7 @@ class ZoneServiceSpec
.when(mockZoneRepo)
.getZone(zoneNotAuthorized.id)
- val error = leftResultOf(underTest.listZoneChanges(zoneNotAuthorized.id, okAuth).value)
+ val error = underTest.listZoneChanges(zoneNotAuthorized.id, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -764,31 +1326,63 @@ class ZoneServiceSpec
.listZoneChanges(zoneId = okZone.id, startFrom = None, maxItems = 100)
val result: ListZoneChangesResponse =
- rightResultOf(underTest.listZoneChanges(okZone.id, okAuth).value)
+ underTest.listZoneChanges(okZone.id, okAuth).value.unsafeRunSync().toOption.get
result.zoneChanges.head shouldBe zoneUpdate
result.zoneChanges(1) shouldBe zoneCreate
}
}
+ "listFailedZoneChanges" should {
+ "retrieve the zone changes" in {
+
+ doReturn(IO.pure(ListFailedZoneChangesResults(
+ List(zoneUpdate.copy(status = ZoneChangeStatus.Failed),zoneCreate.copy(status = ZoneChangeStatus.Failed))
+ )))
+ .when(mockZoneChangeRepo)
+ .listFailedZoneChanges(100,0)
+
+ val result: ListFailedZoneChangesResponse =
+ underTest.listFailedZoneChanges(okAuth).value.unsafeRunSync().toOption.get
+
+ result.failedZoneChanges shouldBe
+ List(zoneUpdate.copy(status = ZoneChangeStatus.Failed),zoneCreate.copy(status = ZoneChangeStatus.Failed))
+ result.failedZoneChanges.head shouldBe zoneUpdate.copy(status = ZoneChangeStatus.Failed)
+ result.failedZoneChanges(1) shouldBe zoneCreate.copy(status = ZoneChangeStatus.Failed)
+ }
+
+ "retrieve the zone changes with startFrom and maxItems" in {
+
+ doReturn(IO.pure(ListFailedZoneChangesResults(
+ List(zoneUpdate.copy(status = ZoneChangeStatus.Failed),zoneCreate.copy(status = ZoneChangeStatus.Failed))
+ ))).when(mockZoneChangeRepo)
+ .listFailedZoneChanges(1,1)
+
+ val result: ListFailedZoneChangesResponse =
+ underTest.listFailedZoneChanges(okAuth,1,1).value.unsafeRunSync().toOption.get
+ result.startFrom shouldBe 1
+ result.nextId shouldBe 0
+ result.maxItems shouldBe 1
+ }
+ }
+
"AddAclRule" should {
"fail if the user is not authorized for the zone" in {
doReturn(IO.pure(Some(zoneNotAuthorized))).when(mockZoneRepo).getZone(anyString)
val error =
- leftResultOf(underTest.addACLRule(zoneNotAuthorized.id, baseAclRuleInfo, okAuth).value)
+ underTest.addACLRule(zoneNotAuthorized.id, baseAclRuleInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
"generate a zone update if the request is valid" in {
doReturn(IO.pure(Some(okZone))).when(mockZoneRepo).getZone(anyString)
- val result: ZoneChange = rightResultOf(
+ val result: ZoneChange =
underTest
.addACLRule(okZone.id, userAclRuleInfo, okAuth)
.map(_.asInstanceOf[ZoneChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.changeType shouldBe ZoneChangeType.Update
result.zone.acl.rules.size shouldBe 1
@@ -800,7 +1394,7 @@ class ZoneServiceSpec
val invalidRegexMaskRuleInfo = baseAclRuleInfo.copy(recordMask = Some("x{5,-3}"))
val error =
- leftResultOf(underTest.addACLRule(okZone.id, invalidRegexMaskRuleInfo, okAuth).value)
+ underTest.addACLRule(okZone.id, invalidRegexMaskRuleInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe an[InvalidRequest]
}
}
@@ -810,7 +1404,7 @@ class ZoneServiceSpec
doReturn(IO.pure(Some(zoneNotAuthorized))).when(mockZoneRepo).getZone(anyString)
val error =
- leftResultOf(underTest.deleteACLRule(zoneNotAuthorized.id, baseAclRuleInfo, okAuth).value)
+ underTest.deleteACLRule(zoneNotAuthorized.id, baseAclRuleInfo, okAuth).value.unsafeRunSync().swap.toOption.get
error shouldBe a[NotAuthorizedError]
}
@@ -819,12 +1413,11 @@ class ZoneServiceSpec
val zone = okZone.copy(acl = acl)
doReturn(IO.pure(Some(zone))).when(mockZoneRepo).getZone(anyString)
- val result: ZoneChange = rightResultOf(
+ val result: ZoneChange =
underTest
.deleteACLRule(zone.id, userAclRuleInfo, okAuth)
.map(_.asInstanceOf[ZoneChange])
- .value
- )
+ .value.unsafeRunSync().toOption.get
result.changeType shouldBe ZoneChangeType.Update
result.zone.acl.rules.size shouldBe 0
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandlerSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandlerSpec.scala
new file mode 100644
index 000000000..bdf36b33b
--- /dev/null
+++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneSyncScheduleHandlerSpec.scala
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2018 Comcast Cable Communications Management, LLC
+ *
+ * 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 vinyldns.api.domain.zone
+
+import cats.effect.IO
+import cats.scalatest.ValidatedMatchers
+import org.mockito.Mockito.doReturn
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import org.scalatestplus.mockito.MockitoSugar.mock
+import vinyldns.core.TestZoneData._
+import vinyldns.core.domain.zone.{ZoneRepository, ZoneStatus}
+
+class ZoneSyncScheduleHandlerSpec extends AnyWordSpec with Matchers with ValidatedMatchers {
+ import vinyldns.api.domain.zone.ZoneSyncScheduleHandler._
+
+ private val mockZoneRepo = mock[ZoneRepository]
+ private val okZoneWithSchedule = okZone.copy(recurrenceSchedule = Some("0/1 * * ? * *"), scheduleRequestor = Some("okUser"))
+ private val xyzZoneWithSchedule = xyzZone.copy(recurrenceSchedule = Some("0/1 * * ? * *"), scheduleRequestor = Some("xyzUser"))
+
+
+ "getZoneWithSchedule" should {
+ "get zones which are scheduled for zone sync" in {
+ val zones = List(okZoneWithSchedule, abcZone, xyzZoneWithSchedule)
+ val result = getZonesWithSchedule(zones)
+
+ result shouldBe List(okZoneWithSchedule.id, xyzZoneWithSchedule.id)
+ }
+
+ "get empty list when no zone is scheduled for zone sync" in {
+ val zones = List(xyzZone, abcZone)
+ val result = getZonesWithSchedule(zones)
+
+ result shouldBe List.empty
+ }
+ }
+
+ "getZoneChanges" should {
+ "return zone changes for zones that have zone sync scheduled" in {
+ doReturn(IO.pure(Set(okZoneWithSchedule, xyzZoneWithSchedule))).when(mockZoneRepo).getZones(Set(okZoneWithSchedule.id, xyzZoneWithSchedule.id))
+ val result = getZoneChanges(mockZoneRepo, List(okZoneWithSchedule.id, xyzZoneWithSchedule.id)).unsafeRunSync()
+
+ result.map(zoneChange => zoneChange.zone.name) shouldBe Set(okZoneWithSchedule.name, xyzZoneWithSchedule.name)
+ result.map(zoneChange => zoneChange.zone.recurrenceSchedule) shouldBe Set(okZoneWithSchedule.recurrenceSchedule, xyzZoneWithSchedule.recurrenceSchedule)
+ result.map(zoneChange => zoneChange.zone.scheduleRequestor) shouldBe Set(okZoneWithSchedule.scheduleRequestor, xyzZoneWithSchedule.scheduleRequestor)
+ result.map(zoneChange => zoneChange.zone.status) shouldBe Set(okZoneWithSchedule.copy(status = ZoneStatus.Syncing).status, xyzZoneWithSchedule.copy(status = ZoneStatus.Syncing).status)
+ }
+ }
+
+ "zoneSyncScheduler" should {
+ "return zones that have zone sync scheduled" in {
+ doReturn(IO.pure(Set(okZoneWithSchedule, xyzZoneWithSchedule))).when(mockZoneRepo).getAllZonesWithSyncSchedule
+ doReturn(IO.pure(Set(okZoneWithSchedule, xyzZoneWithSchedule))).when(mockZoneRepo).getZones(Set(okZoneWithSchedule.id, xyzZoneWithSchedule.id, xyzZone.id, okZone.id))
+ val result = zoneSyncScheduler(mockZoneRepo).unsafeRunSync()
+
+ // We only get the 2 zones which have zone sync schedule
+ result.size shouldBe 2
+ result.map(zoneChange => zoneChange.zone.name) shouldBe Set(okZoneWithSchedule.name, xyzZoneWithSchedule.name)
+ result.map(zoneChange => zoneChange.zone.recurrenceSchedule) shouldBe Set(okZoneWithSchedule.recurrenceSchedule, xyzZoneWithSchedule.recurrenceSchedule)
+ result.map(zoneChange => zoneChange.zone.scheduleRequestor) shouldBe Set(okZoneWithSchedule.scheduleRequestor, xyzZoneWithSchedule.scheduleRequestor)
+ result.map(zoneChange => zoneChange.zone.status) shouldBe Set(okZoneWithSchedule.copy(status = ZoneStatus.Syncing).status, xyzZoneWithSchedule.copy(status = ZoneStatus.Syncing).status)
+ }
+ }
+}
diff --git a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneViewLoaderSpec.scala b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneViewLoaderSpec.scala
index a3f5e83de..5d636d75c 100644
--- a/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneViewLoaderSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/domain/zone/ZoneViewLoaderSpec.scala
@@ -32,10 +32,11 @@ import vinyldns.core.domain.record._
import scala.collection.mutable
import cats.effect._
import vinyldns.api.backend.dns.DnsConversions
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.domain.backend.{Backend, BackendResolver}
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.domain.zone.{Zone, ZoneConnection, ZoneStatus}
class ZoneViewLoaderSpec extends AnyWordSpec with Matchers with MockitoSugar with DnsConversions {
@@ -43,7 +44,7 @@ class ZoneViewLoaderSpec extends AnyWordSpec with Matchers with MockitoSugar wit
private val testZoneName = "vinyldns."
private val testZoneConnection: Option[ZoneConnection] = Some(
- ZoneConnection(testZoneName, testZoneName, "nzisn+4G2ldMn0q1CV3vsg==", "127.0.0.1:19001")
+ ZoneConnection(testZoneName, testZoneName, Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "127.0.0.1:19001")
)
private val mockBackendResolver = mock[BackendResolver]
@@ -95,7 +96,7 @@ class ZoneViewLoaderSpec extends AnyWordSpec with Matchers with MockitoSugar wit
val mockRecordSetDataRepo = mock[RecordSetCacheRepository]
- doReturn(IO(ListRecordSetResults(records, None, None, None, None, None, None, NameSort.ASC)))
+ doReturn(IO(ListRecordSetResults(records, None, None, None, None, None, None, NameSort.ASC, RecordTypeSort.NONE)))
.when(mockRecordSetRepo)
.listRecordSets(
any[Option[String]],
@@ -104,7 +105,8 @@ class ZoneViewLoaderSpec extends AnyWordSpec with Matchers with MockitoSugar wit
any[Option[String]],
any[Option[Set[RecordType]]],
any[Option[String]],
- any[NameSort]
+ any[NameSort],
+ any[RecordTypeSort]
)
val underTest = VinylDNSZoneViewLoader(testZone, mockRecordSetRepo, mockRecordSetDataRepo)
diff --git a/modules/api/src/test/scala/vinyldns/api/engine/BatchChangeHandlerSpec.scala b/modules/api/src/test/scala/vinyldns/api/engine/BatchChangeHandlerSpec.scala
index ce2561f28..9a044b51f 100644
--- a/modules/api/src/test/scala/vinyldns/api/engine/BatchChangeHandlerSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/engine/BatchChangeHandlerSpec.scala
@@ -24,7 +24,6 @@ import org.mockito.Mockito.{doReturn, verify}
import org.scalatest.BeforeAndAfterEach
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.mockito.MockitoSugar
-import vinyldns.api.CatsHelpers
import vinyldns.api.repository.InMemoryBatchChangeRepository
import vinyldns.core.domain.batch._
import vinyldns.core.domain.record._
@@ -35,8 +34,7 @@ import scala.concurrent.ExecutionContext
class BatchChangeHandlerSpec
extends AnyWordSpec
with MockitoSugar
- with BeforeAndAfterEach
- with CatsHelpers {
+ with BeforeAndAfterEach {
implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.global
implicit val contextShift: ContextShift[IO] = IO.contextShift(ec)
@@ -77,7 +75,7 @@ class BatchChangeHandlerSpec
"notify on batch change complete" in {
doReturn(IO.unit).when(mockNotifier).notify(any[Notification[_]])
- await(batchRepo.save(completedBatchChange))
+ batchRepo.save(completedBatchChange).unsafeRunSync()
BatchChangeHandler
.process(batchRepo, notifiers, BatchChangeCommand(completedBatchChange.id))
@@ -92,7 +90,7 @@ class BatchChangeHandlerSpec
val partiallyFailedBatchChange =
completedBatchChange.copy(changes = List(addChange.copy(status = SingleChangeStatus.Failed)))
- await(batchRepo.save(partiallyFailedBatchChange))
+ batchRepo.save(partiallyFailedBatchChange).unsafeRunSync()
BatchChangeHandler
.process(batchRepo, notifiers, BatchChangeCommand(partiallyFailedBatchChange.id))
diff --git a/modules/api/src/test/scala/vinyldns/api/engine/RecordSetChangeHandlerSpec.scala b/modules/api/src/test/scala/vinyldns/api/engine/RecordSetChangeHandlerSpec.scala
index 5ed73ebc4..743097be8 100644
--- a/modules/api/src/test/scala/vinyldns/api/engine/RecordSetChangeHandlerSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/engine/RecordSetChangeHandlerSpec.scala
@@ -27,9 +27,8 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.{BeforeAndAfterEach, EitherValues}
import vinyldns.api.backend.dns.DnsProtocol.{NotAuthorized, TryAgain}
-import vinyldns.api.engine.RecordSetChangeHandler.{AlreadyApplied, ReadyToApply, Requeue}
+import vinyldns.api.engine.RecordSetChangeHandler.{AlreadyApplied, ReadyToApply, Requeue, Failure}
import vinyldns.api.repository.InMemoryBatchChangeRepository
-import vinyldns.api.CatsHelpers
import vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChangeStatus}
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record.{ChangeSet, RecordChangeRepository, RecordSetRepository, _}
@@ -46,7 +45,6 @@ class RecordSetChangeHandlerSpec
with Matchers
with MockitoSugar
with BeforeAndAfterEach
- with CatsHelpers
with EitherValues
with TransactionProvider {
@@ -116,7 +114,7 @@ class RecordSetChangeHandlerSpec
batchRepo.clear()
// seed the linked batch change in the DB
- await(batchRepo.save(batchChange))
+ batchRepo.save(batchChange).unsafeRunSync()
doReturn(IO.pure(Nil))
.when(mockRsRepo)
@@ -151,7 +149,7 @@ class RecordSetChangeHandlerSpec
savedCs.status shouldBe ChangeSetStatus.Complete
savedCs.changes.head.status shouldBe RecordSetChangeStatus.Complete
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
systemMessage= None,
@@ -197,7 +195,7 @@ class RecordSetChangeHandlerSpec
verify(mockBackend).applyChange(rsChange)
verify(mockBackend, times(2)).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
systemMessage= None,
@@ -248,7 +246,7 @@ class RecordSetChangeHandlerSpec
// make sure we only called resolve once when validating, ensures that verify was not called
verify(mockBackend, times(1)).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Failed,
@@ -294,7 +292,7 @@ class RecordSetChangeHandlerSpec
// we will retry the verify 3 times based on the mock setup
verify(mockBackend, times(2)).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Failed,
@@ -350,7 +348,7 @@ class RecordSetChangeHandlerSpec
verify(mockBackend, never()).applyChange(rsChange)
verify(mockBackend, times(1)).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Failed,
@@ -393,7 +391,7 @@ class RecordSetChangeHandlerSpec
verify(mockBackend, times(1)).applyChange(rsChange)
verify(mockBackend, times(1)).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Failed,
@@ -448,7 +446,7 @@ class RecordSetChangeHandlerSpec
// make sure we never called resolve, as we skip validate step and verify
verify(mockBackend, never).resolve(rs.name, rsChange.zone.name, rs.typ)
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
status = SingleChangeStatus.Complete,
@@ -602,7 +600,7 @@ class RecordSetChangeHandlerSpec
savedCs.status shouldBe ChangeSetStatus.Complete
savedCs.changes.head.status shouldBe RecordSetChangeStatus.Complete
- val batchChangeUpdates = await(batchRepo.getBatchChange(batchChange.id))
+ val batchChangeUpdates = batchRepo.getBatchChange(batchChange.id).unsafeRunSync()
val updatedSingleChanges = completeCreateAAAASingleChanges.map { ch =>
ch.copy(
systemMessage= None,
@@ -647,9 +645,7 @@ class RecordSetChangeHandlerSpec
changeSet.status shouldBe RecordSetChangeStatus.Failed
changeSet.recordSet.status shouldBe RecordSetStatus.Inactive
changeSet.systemMessage shouldBe Some(
- s"""Failed validating update to DNS for change "${changeSet.id}": "${changeSet.recordSet.name}": """ +
- s"This record set is out of sync with the DNS backend; sync this zone before attempting to " +
- "update this record set."
+ "This record set is out of sync with the DNS backend. Sync this zone before attempting to update this record set."
)
val savedCs = changeRepoCaptor.getValue
@@ -871,6 +867,25 @@ class RecordSetChangeHandlerSpec
processorStatus shouldBe a[AlreadyApplied]
}
+ "return Failed if there is a record in the DNS backend but not in vinyldns, and we try to delete it " in {
+ doReturn(IO.pure(List(rs)))
+ .when(mockBackend)
+ .resolve(rs.name, rsChange.zone.name, rs.typ)
+ doReturn(IO.pure(List(rs))).when(mockRsRepo).getRecordSetsByName(cs.zoneId, rs.name)
+
+ val processorStatus = RecordSetChangeHandler
+ .syncAndGetProcessingStatusFromDnsBackend(
+ rsChange.copy(changeType = RecordSetChangeType.Sync),
+ mockBackend,
+ mockRsRepo,
+ mockChangeRepo,
+ mockRecordSetDataRepo,
+ true
+ )
+ .unsafeRunSync()
+ processorStatus shouldBe a[Failure]
+ }
+
"sync in the DNS backend for Delete change if record exists" in {
doReturn(IO.pure(List(rs)))
.when(mockBackend)
diff --git a/modules/api/src/test/scala/vinyldns/api/engine/ZoneSyncHandlerSpec.scala b/modules/api/src/test/scala/vinyldns/api/engine/ZoneSyncHandlerSpec.scala
index 7e7a10b47..b566abceb 100644
--- a/modules/api/src/test/scala/vinyldns/api/engine/ZoneSyncHandlerSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/engine/ZoneSyncHandlerSpec.scala
@@ -30,7 +30,7 @@ import scalikejdbc.{ConnectionPool, DB}
import vinyldns.api.VinylDNSTestHelpers
import vinyldns.api.domain.record.RecordSetChangeGenerator
import vinyldns.api.domain.zone.{DnsZoneViewLoader, VinylDNSZoneViewLoader, ZoneView}
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.domain.backend.{Backend, BackendResolver}
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
@@ -40,6 +40,7 @@ import vinyldns.core.domain.zone._
import cats.syntax.all._
import org.slf4j.{Logger, LoggerFactory}
import vinyldns.api.engine.ZoneSyncHandler.{monitor, time}
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.mysql.TransactionProvider
class ZoneSyncHandlerSpec
@@ -181,16 +182,16 @@ class ZoneSyncHandlerSpec
zoneName,
"test@test.com",
ZoneStatus.Active,
- connection = Some(ZoneConnection(zoneName, dnsKeyName, dnsTsig, dnsServerAddress)),
- transferConnection = Some(ZoneConnection(zoneName, dnsKeyName, dnsTsig, dnsServerAddress))
+ connection = Some(ZoneConnection(zoneName, dnsKeyName, Encrypted(dnsTsig), dnsServerAddress)),
+ transferConnection = Some(ZoneConnection(zoneName, dnsKeyName, Encrypted(dnsTsig), dnsServerAddress))
)
private val testReverseZone = Zone(
reverseZoneName,
"test@test.com",
ZoneStatus.Active,
- connection = Some(ZoneConnection(zoneName, dnsKeyName, dnsTsig, dnsServerAddress)),
- transferConnection = Some(ZoneConnection(zoneName, dnsKeyName, dnsTsig, dnsServerAddress))
+ connection = Some(ZoneConnection(zoneName, dnsKeyName, Encrypted(dnsTsig), dnsServerAddress)),
+ transferConnection = Some(ZoneConnection(zoneName, dnsKeyName, Encrypted(dnsTsig), dnsServerAddress))
)
private val testRecord1 = RecordSet(
@@ -311,7 +312,7 @@ class ZoneSyncHandlerSpec
)
doReturn(
- IO(ListRecordSetResults(List(testRecord1), None, None, None, None, None, None, NameSort.ASC))
+ IO(ListRecordSetResults(List(testRecord1), None, None, None, None, None, None, NameSort.ASC, recordTypeSort = RecordTypeSort.NONE))
).when(recordSetRepo)
.listRecordSets(
any[Option[String]],
@@ -320,7 +321,8 @@ class ZoneSyncHandlerSpec
any[Option[String]],
any[Option[Set[RecordType]]],
any[Option[String]],
- any[NameSort]
+ any[NameSort],
+ any[RecordTypeSort],
)
doReturn(IO(testChangeSet)).when(recordSetRepo).apply(any[DB], any[ChangeSet])
@@ -554,7 +556,8 @@ class ZoneSyncHandlerSpec
verify(recordChangeRepo).save(any[DB], captor.capture())
val req = captor.getValue
- anonymize(req) shouldBe anonymize(testChangeSet)
+ val expectedRecordSetChanges = testChangeSet.changes
+ anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
}
"save the record changes to the recordSetCacheRepo" in {
@@ -564,7 +567,8 @@ class ZoneSyncHandlerSpec
verify(recordSetCacheRepo).save(any[DB], captor.capture())
val req = captor.getValue
- anonymize(req) shouldBe anonymize(testChangeSet)
+ val expectedRecordSetChanges = testChangeSet.changes
+ anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
}
@@ -575,7 +579,8 @@ class ZoneSyncHandlerSpec
verify(recordSetRepo).apply(any[DB], captor.capture())
val req = captor.getValue
- anonymize(req) shouldBe anonymize(testChangeSet)
+ val expectedRecordSetChanges = testChangeSet.changes
+ anonymize(req) shouldBe anonymize(testChangeSet.withRecordSetChange(expectedRecordSetChanges))
}
"returns the zone as active and sets the latest sync" in {
diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala
index ec12601ba..243ac3546 100644
--- a/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/notifier/email/EmailNotifierSpec.scala
@@ -20,28 +20,33 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import org.scalatestplus.mockito.MockitoSugar
import vinyldns.api.CatsHelpers
+
import javax.mail.{Provider, Session, Transport, URLName}
import java.util.Properties
-
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, User, UserRepository}
import vinyldns.core.notifier.Notification
+
import javax.mail.internet.InternetAddress
import org.mockito.Matchers.{eq => eqArg, _}
import org.mockito.Mockito._
import org.mockito.ArgumentCaptor
import cats.effect.IO
+
import javax.mail.{Address, Message}
-import vinyldns.core.domain.membership.User
import _root_.vinyldns.core.domain.batch._
+
import java.time.Instant
import java.time.temporal.ChronoUnit
-import vinyldns.core.domain.record.RecordType
-import vinyldns.core.domain.record.AData
+import vinyldns.core.domain.record.{AData, OwnerShipTransferStatus, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
+import vinyldns.core.domain.Encrypted
import scala.collection.JavaConverters._
import vinyldns.core.notifier.NotifierConfig
+import vinyldns.core.TestMembershipData.{dummyGroup, dummyUser, okGroup, okUser}
+import vinyldns.core.TestRecordSetData.{ownerShipTransfer, rsOk}
+import vinyldns.core.TestZoneData.okZone
object MockTransport extends MockitoSugar {
val mockTransport: Transport = mock[Transport]
@@ -68,6 +73,7 @@ class EmailNotifierSpec
import MockTransport._
val mockUserRepository: UserRepository = mock[UserRepository]
+ val mockGroupRepository: GroupRepository = mock[GroupRepository]
val session: Session = Session.getInstance(new Properties())
session.setProvider(
new Provider(
@@ -100,6 +106,17 @@ class EmailNotifierSpec
"testBatch"
)
+
+ def reccordSetChange: RecordSetChange =
+ RecordSetChange(
+ okZone,
+ rsOk.copy(ownerGroupId= Some(okGroup.id),recordSetGroupChange =
+ Some(ownerShipTransfer.copy(ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview, requestedOwnerGroupId = Some(dummyGroup.id)))),
+ "system",
+ RecordSetChangeType.Create,
+ RecordSetChangeStatus.Complete
+ )
+
"Email Notifier" should {
"do nothing for unsupported Notifications" in {
val emailConfig: Config = ConfigFactory.parseMap(
@@ -110,7 +127,7 @@ class EmailNotifierSpec
).asJava
)
val notifier = new EmailNotifierProvider()
- .load(NotifierConfig("", emailConfig), mockUserRepository)
+ .load(NotifierConfig("", emailConfig), mockUserRepository, mockGroupRepository)
.unsafeRunSync()
notifier.notify(new Notification("this won't be supported ever")) should be(IO.unit)
@@ -120,10 +137,11 @@ class EmailNotifierSpec
val notifier = new EmailNotifier(
EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()),
session,
- mockUserRepository
+ mockUserRepository,
+ mockGroupRepository
)
- doReturn(IO.pure(Some(User("testUser", "access", "secret"))))
+ doReturn(IO.pure(Some(User("testUser", "access", Encrypted("secret")))))
.when(mockUserRepository)
.getUser("test")
@@ -132,11 +150,60 @@ class EmailNotifierSpec
verify(mockUserRepository).getUser("test")
}
+ "send an email to a user for recordSet ownership transfer" in {
+ val fromAddress = new InternetAddress("test@test.com")
+
+ val notifier = new EmailNotifier(
+ EmailNotifierConfig(fromAddress, new Properties()),
+ session,
+ mockUserRepository,
+ mockGroupRepository
+ )
+ val expectedAddresses = Array[Address](new InternetAddress("test@test.com"),new InternetAddress("test@test.com"))
+ val messageArgument = ArgumentCaptor.forClass(classOf[Message])
+ val dummyGrp = dummyGroup.copy(memberIds = Set(dummyUser.id))
+ val dummyUsr = dummyUser.copy(id=dummyUser.id,email = Some("test@test.com"))
+
+ doNothing().when(mockTransport).connect()
+ doNothing()
+ .when(mockTransport)
+ .sendMessage(messageArgument.capture(), eqArg(expectedAddresses))
+ doNothing().when(mockTransport).close()
+ doReturn(IO.pure(Some(okGroup)))
+ .when(mockGroupRepository)
+ .getGroup(okGroup.id)
+ doReturn(IO.pure(Some(dummyGrp)))
+ .when(mockGroupRepository)
+ .getGroup(dummyGrp.id)
+ doReturn(IO.pure(Some(okUser)))
+ .when(mockUserRepository)
+ .getUser(okUser.id)
+ doReturn(IO.pure(Some(dummyUsr)))
+ .when(mockUserRepository)
+ .getUser(dummyGrp.memberIds.head)
+
+ val rsc = reccordSetChange.copy(userId = okUser.id)
+
+ notifier.notify(Notification(rsc)).unsafeRunSync()
+ val message = messageArgument.getValue
+ message.getFrom should be(Array(fromAddress))
+ message.getContentType should be("text/html; charset=us-ascii")
+ message.getAllRecipients should be(expectedAddresses)
+ message.getSubject should be(s"VinylDNS RecordSet change ${rsc.id} results")
+
+ val content = message.getContent.asInstanceOf[String]
+
+ content.contains(rsc.id) should be(true)
+ content.contains(rsc.recordSet.ownerGroupId.get) should be(true)
+ content.contains(rsc.recordSet.recordSetGroupChange.map(_.requestedOwnerGroupId.get).get) should be(true)
+ }
+
"do nothing when user not found" in {
val notifier = new EmailNotifier(
EmailNotifierConfig(new InternetAddress("test@test.com"), new Properties()),
session,
- mockUserRepository
+ mockUserRepository,
+ mockGroupRepository
)
doReturn(IO.pure(None))
@@ -148,16 +215,17 @@ class EmailNotifierSpec
verify(mockUserRepository).getUser("test")
}
- "send an email to a user" in {
+ "send an email to a user for batch change" in {
val fromAddress = new InternetAddress("test@test.com")
val notifier = new EmailNotifier(
EmailNotifierConfig(fromAddress, new Properties()),
session,
- mockUserRepository
+ mockUserRepository,
+ mockGroupRepository
)
doReturn(
- IO.pure(Some(User("testUser", "access", "secret", None, None, Some("testuser@test.com"))))
+ IO.pure(Some(User("testUser", "access", Encrypted("secret"), None, None, Some("testuser@test.com"))))
).when(mockUserRepository)
.getUser("test")
diff --git a/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala b/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala
index 2adc41e7c..fe04879fa 100644
--- a/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/notifier/sns/SnsNotifierSpec.scala
@@ -20,21 +20,17 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import org.scalatestplus.mockito.MockitoSugar
import vinyldns.api.CatsHelpers
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import vinyldns.core.domain.membership.UserRepository
import vinyldns.core.notifier.Notification
import org.mockito.Matchers._
import org.mockito.Mockito._
import org.mockito.ArgumentCaptor
import cats.effect.IO
-import vinyldns.core.domain.batch.BatchChange
+import _root_.vinyldns.core.domain.batch.{BatchChange, BatchChangeApprovalStatus, SingleAddChange, SingleChange, SingleChangeStatus, SingleDeleteRRSetChange}
import java.time.Instant
-import vinyldns.core.domain.batch.BatchChangeApprovalStatus
-import vinyldns.core.domain.batch.SingleChange
-import vinyldns.core.domain.batch.SingleAddChange
-import vinyldns.core.domain.batch.SingleDeleteRRSetChange
import vinyldns.core.domain.record.RecordType
import vinyldns.core.domain.record.AData
-import _root_.vinyldns.core.domain.batch.SingleChangeStatus
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import scala.collection.JavaConverters._
@@ -51,6 +47,7 @@ class SnsNotifierSpec
with CatsHelpers {
val mockUserRepository = mock[UserRepository]
+ val mockGroupRepository = mock[GroupRepository]
val mockSns = mock[AmazonSNS]
override protected def beforeEach(): Unit =
@@ -86,7 +83,7 @@ class SnsNotifierSpec
).asJava
)
val notifier = new SnsNotifierProvider()
- .load(NotifierConfig("", snsConfig), mockUserRepository)
+ .load(NotifierConfig("", snsConfig), mockUserRepository, mockGroupRepository)
.unsafeRunSync()
notifier.notify(Notification("this won't be supported ever")) should be(IO.unit)
diff --git a/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala b/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala
index 70893e3de..b9dd1c49e 100644
--- a/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala
+++ b/modules/api/src/test/scala/vinyldns/api/repository/EmptyRepositories.scala
@@ -19,11 +19,12 @@ package vinyldns.api.repository
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record._
-import vinyldns.core.domain.zone.{Zone, ZoneRepository, ListZonesResults}
+import vinyldns.core.domain.zone.{ListZonesResults, Zone, ZoneRepository}
import cats.effect._
import scalikejdbc._
import vinyldns.core.domain.membership._
import vinyldns.core.domain.record.NameSort.NameSort
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError
// Empty implementations let our other test classes just edit with the methods they need
@@ -42,9 +43,10 @@ trait EmptyRecordSetRepo extends RecordSetRepository {
recordNameFilter: Option[String],
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
- nameSort: NameSort
+ nameSort: NameSort,
+ recordTypeSort: RecordTypeSort
): IO[ListRecordSetResults] =
- IO.pure(ListRecordSetResults(nameSort = nameSort))
+ IO.pure(ListRecordSetResults(nameSort = nameSort,recordTypeSort=recordTypeSort))
def getRecordSets(zoneId: String, name: String, typ: RecordType): IO[List[RecordSet]] =
@@ -73,7 +75,7 @@ trait EmptyRecordSetCacheRepo extends RecordSetCacheRepository {
recordOwnerGroupFilter: Option[String],
nameSort: NameSort
): IO[ListRecordSetResults] =
- IO.pure(ListRecordSetResults(nameSort = nameSort))
+ IO.pure(ListRecordSetResults(nameSort = nameSort, recordTypeSort = RecordTypeSort.NONE))
}
trait EmptyZoneRepo extends ZoneRepository {
@@ -89,7 +91,8 @@ trait EmptyZoneRepo extends ZoneRepository {
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults] = IO.pure(ListZonesResults())
def listZones(
@@ -97,7 +100,8 @@ trait EmptyZoneRepo extends ZoneRepository {
zoneNameFilter: Option[String] = None,
startFrom: Option[String] = None,
maxItems: Int = 100,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults] = IO.pure(ListZonesResults())
def getZonesByAdminGroupId(adminGroupId: String): IO[List[Zone]] = IO.pure(List())
diff --git a/modules/api/src/test/scala/vinyldns/api/repository/InMemoryBatchChangeRepository.scala b/modules/api/src/test/scala/vinyldns/api/repository/InMemoryBatchChangeRepository.scala
index 06ebf7ccf..3a6ba1be4 100644
--- a/modules/api/src/test/scala/vinyldns/api/repository/InMemoryBatchChangeRepository.scala
+++ b/modules/api/src/test/scala/vinyldns/api/repository/InMemoryBatchChangeRepository.scala
@@ -16,13 +16,15 @@
package vinyldns.api.repository
-import java.time.Instant
+import java.time.{Instant, LocalDateTime, ZoneId}
+import java.time.format.DateTimeFormatter
import vinyldns.core.domain.batch._
import scala.collection.concurrent
import cats.effect._
import cats.implicits._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
class InMemoryBatchChangeRepository extends BatchChangeRepository {
@@ -112,12 +114,27 @@ class InMemoryBatchChangeRepository extends BatchChangeRepository {
def getBatchChangeSummaries(
userId: Option[String],
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int] = None,
maxItems: Int = 100,
+ batchStatus: Option[BatchChangeStatus] = None,
approvalStatus: Option[BatchChangeApprovalStatus] = None
): IO[BatchChangeSummaryList] = {
+
+ // Define the desired date-time format
+ val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+
+ // Convert strings to Instant
+ val startInstant = dateTimeStartRange.map(dt => LocalDateTime.parse(dt, formatter).atZone(ZoneId.of("UTC")).toInstant)
+ val endInstant = dateTimeEndRange.map(dt => LocalDateTime.parse(dt, formatter).atZone(ZoneId.of("UTC")).toInstant)
+
val userBatchChanges = batches.values.toList
.filter(b => userId.forall(_ == b.userId))
+ .filter(bu => userName.forall(_ == bu.userName))
+ .filter(bdtsi => startInstant.forall(_.isBefore(bdtsi.createdTimestamp)))
+ .filter(bdtei => endInstant.forall(_.isAfter(bdtei.createdTimestamp)))
.filter(as => approvalStatus.forall(_ == as.approvalStatus))
val batchChangeSummaries = for {
sc <- userBatchChanges
@@ -150,7 +167,11 @@ class InMemoryBatchChangeRepository extends BatchChangeRepository {
nextId = nextId,
maxItems = maxItems,
ignoreAccess = ignoreAccess,
- approvalStatus = approvalStatus
+ batchStatus = batchStatus,
+ approvalStatus = approvalStatus,
+ userName = userName,
+ dateTimeStartRange = dateTimeStartRange,
+ dateTimeEndRange = dateTimeEndRange
)
)
}
diff --git a/modules/api/src/test/scala/vinyldns/api/route/BatchChangeJsonProtocolSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/BatchChangeJsonProtocolSpec.scala
index eccdc6ce9..53ad40cb0 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/BatchChangeJsonProtocolSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/BatchChangeJsonProtocolSpec.scala
@@ -127,15 +127,15 @@ class BatchChangeJsonProtocolSpec
addCNAMEChangeInputJson
)
- val addAChangeInput = AddChangeInput("foo.", A, Some(3600), AData("1.1.1.1"))
+ val addAChangeInput = AddChangeInput("foo.", A, None, Some(3600), AData("1.1.1.1"))
- val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A)
+ val deleteAChangeInput = DeleteRRSetChangeInput("foo.", A, None)
- val addAAAAChangeInput = AddChangeInput("bar.", AAAA, Some(1200), AAAAData("1:2:3:4:5:6:7:8"))
+ val addAAAAChangeInput = AddChangeInput("bar.", AAAA, None, Some(1200), AAAAData("1:2:3:4:5:6:7:8"))
- val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, Some(200), CNAMEData(Fqdn("buzz.")))
+ val addCNAMEChangeInput = AddChangeInput("bizz.baz.", CNAME, None, Some(200), CNAMEData(Fqdn("buzz.")))
- val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, Some(200), PTRData(Fqdn("test.com.")))
+ val addPTRChangeInput = AddChangeInput("4.5.6.7", PTR, None, Some(200), PTRData(Fqdn("test.com.")))
val fooDiscoveryError = ZoneDiscoveryError("foo.")
@@ -180,7 +180,7 @@ class BatchChangeJsonProtocolSpec
val result = ChangeInputSerializer.fromJson(json)
result should haveInvalid(
- s"Unsupported type $UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, and MX"
+ s"Unsupported type $UNKNOWN, valid types include: A, AAAA, CNAME, PTR, TXT, MX, NS, SRV and NAPTR"
)
}
@@ -211,7 +211,7 @@ class BatchChangeJsonProtocolSpec
)
val result = ChangeInputSerializer.fromJson(json).value
- result shouldBe AddChangeInput("foo.", A, None, AData("1.1.1.1"))
+ result shouldBe AddChangeInput("foo.", A, None, None, AData("1.1.1.1"))
}
"return an error if the record is not specified for add" in {
@@ -225,11 +225,32 @@ class BatchChangeJsonProtocolSpec
resultAAAA should haveInvalid("Missing BatchChangeInput.changes.record.address")
}
+
+ "return an error if the record data is not specified for NS" in {
+ val jsonNS = buildAddChangeInputJson(Some("foo."), Some(NS), Some(3600))
+ val resultNS = ChangeInputSerializer.fromJson(jsonNS)
+
+ resultNS should haveInvalid("Missing BatchChangeInput.changes.record.nsdname")
+ }
+
+ "return an error if the record data is not specified for SRV" in {
+ val jsonSRV = buildAddChangeInputJson(Some("foo."), Some(SRV), Some(3600))
+ val resultSRV = ChangeInputSerializer.fromJson(jsonSRV)
+
+ resultSRV should haveInvalid("Missing BatchChangeInput.changes.record.priority and Missing BatchChangeInput.changes.record.weight and Missing BatchChangeInput.changes.record.port and Missing BatchChangeInput.changes.record.target")
+ }
+
+ "return an error if the record data is not specified for NAPTR" in {
+ val jsonNAPTR = buildAddChangeInputJson(Some("foo."), Some(NAPTR), Some(3600))
+ val resultNAPTR = ChangeInputSerializer.fromJson(jsonNAPTR)
+
+ resultNAPTR should haveInvalid("Missing BatchChangeInput.changes.record.order and Missing BatchChangeInput.changes.record.preference and Missing BatchChangeInput.changes.record.flags and Missing BatchChangeInput.changes.record.service and Missing BatchChangeInput.changes.record.regexp and Missing BatchChangeInput.changes.record.replacement")
+ }
}
"serializing ChangeInputSerializer to JSON" should {
"successfully serialize valid data for delete" in {
- val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, Some(AData("1.1.1.1")))
+ val deleteChangeInput = DeleteRRSetChangeInput("foo.", A, None, Some(AData("1.1.1.1")))
val json: JObject = buildDeleteRRSetInputJson(Some("foo."), Some(A), Some(AData("1.1.1.1")))
val result = ChangeInputSerializer.toJson(deleteChangeInput)
diff --git a/modules/api/src/test/scala/vinyldns/api/route/BatchChangeRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/BatchChangeRoutingSpec.scala
index c8ba4406d..5c97ce77d 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/BatchChangeRoutingSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/BatchChangeRoutingSpec.scala
@@ -38,6 +38,7 @@ import vinyldns.core.TestMembershipData._
import vinyldns.core.domain.BatchChangeIsEmpty
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType._
import vinyldns.core.domain.record._
@@ -310,14 +311,18 @@ class BatchChangeRoutingSpec()
def listBatchChangeSummaries(
auth: AuthPrincipal,
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int],
maxItems: Int,
ignoreAccess: Boolean = false,
+ batchStatus: Option[BatchChangeStatus] = None,
approvalStatus: Option[BatchChangeApprovalStatus] = None
): EitherT[IO, BatchChangeErrorResponse, BatchChangeSummaryList] =
if (auth.userId == okAuth.userId)
- (auth, startFrom, maxItems, ignoreAccess, approvalStatus) match {
- case (_, None, 100, _, None) =>
+ (auth, userName, dateTimeStartRange, dateTimeEndRange, startFrom, maxItems, ignoreAccess, approvalStatus) match {
+ case (_, None, None, None, None, 100, _, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges =
@@ -327,7 +332,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, None, 1, _, None) =>
+ case (_, None, None, None, None, 1, _, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(batchChangeSummaryInfo1),
@@ -337,7 +342,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, Some(1), 100, _, None) =>
+ case (_, None, None, None, Some(1), 100, _, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(batchChangeSummaryInfo2),
@@ -346,7 +351,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, Some(1), 1, _, None) =>
+ case (_, None, None, None, Some(1), 1, _, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(batchChangeSummaryInfo2),
@@ -356,7 +361,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, None, 100, _, Some(BatchChangeApprovalStatus.PendingReview)) =>
+ case (_, None, None, None, None, 100, _, Some(BatchChangeApprovalStatus.PendingReview)) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(batchChangeSummaryInfo2),
@@ -366,11 +371,23 @@ class BatchChangeRoutingSpec()
)
)
+ case (_, Some(okAuth.signedInUser.userName), Some("2023-11-14 00:00:00"), Some("2023-11-15 00:00:00"), None, 100, _, None) =>
+ EitherT.rightT(
+ BatchChangeSummaryList(
+ batchChanges = List(batchChangeSummaryInfo2),
+ startFrom = None,
+ nextId = None,
+ userName = Some(okAuth.signedInUser.userName),
+ dateTimeStartRange = Some("2023-11-14 00:00:00"),
+ dateTimeEndRange = Some("2023-11-15 00:00:00")
+ )
+ )
+
case _ => EitherT.rightT(BatchChangeSummaryList(List()))
}
else if (auth.userId == superUserAuth.userId)
- (auth, startFrom, maxItems, ignoreAccess, approvalStatus) match {
- case (_, None, 100, true, None) =>
+ (auth, userName, dateTimeStartRange, dateTimeEndRange, startFrom, maxItems, ignoreAccess, approvalStatus) match {
+ case (_, None, None, None, None, 100, true, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(
@@ -385,7 +402,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, None, 100, true, Some(BatchChangeApprovalStatus.PendingReview)) => {
+ case (_, None, None, None, None, 100, true, Some(BatchChangeApprovalStatus.PendingReview)) => {
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(batchChangeSummaryInfo2, batchChangeSummaryInfo4),
@@ -397,7 +414,7 @@ class BatchChangeRoutingSpec()
)
}
- case (_, None, 100, false, None) =>
+ case (_, None, None, None, None, 100, false, None) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(),
@@ -406,7 +423,7 @@ class BatchChangeRoutingSpec()
)
)
- case (_, None, 100, false, Some(BatchChangeApprovalStatus.PendingReview)) =>
+ case (_, None, None, None, None, 100, false, Some(BatchChangeApprovalStatus.PendingReview)) =>
EitherT.rightT(
BatchChangeSummaryList(
batchChanges = List(),
@@ -416,6 +433,18 @@ class BatchChangeRoutingSpec()
)
)
+ case (_, Some(superUserAuth.signedInUser.userName), Some("2023-11-14 00:00:00"), Some("2023-11-15 00:00:00"), None, 100, false, None) =>
+ EitherT.rightT(
+ BatchChangeSummaryList(
+ batchChanges = List(),
+ startFrom = None,
+ nextId = None,
+ userName = Some(superUserAuth.signedInUser.userName),
+ dateTimeStartRange = Some("2023-11-14 00:00:00"),
+ dateTimeEndRange = Some("2023-11-15 00:00:00")
+ )
+ )
+
case _ => EitherT.rightT(BatchChangeSummaryList(List()))
}
else
diff --git a/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
index 2289495f0..d02dd339c 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/MembershipRoutingSpec.scala
@@ -19,6 +19,7 @@ package vinyldns.api.route
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
+
import java.time.Instant
import org.json4s.JsonDSL._
import org.json4s._
@@ -32,7 +33,7 @@ import org.scalatest.wordspec.AnyWordSpec
import org.scalatest.BeforeAndAfterEach
import vinyldns.api.Interfaces._
import vinyldns.api.config.LimitsConfig
-import vinyldns.api.domain.membership._
+import vinyldns.api.domain.membership.{UserResponseInfo, _}
import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.api.route.MembershipJsonProtocol.{CreateGroupInput, UpdateGroupInput}
import vinyldns.core.TestMembershipData._
@@ -78,6 +79,8 @@ class MembershipRoutingSpec
val okGroupChangeInfo: GroupChangeInfo = GroupChangeInfo(okGroupChange)
val okGroupChangeUpdateInfo: GroupChangeInfo = GroupChangeInfo(okGroupChangeUpdate)
val okGroupChangeDeleteInfo: GroupChangeInfo = GroupChangeInfo(okGroupChangeDelete)
+ val okUserResponseInfo: UserResponseInfo = UserResponseInfo(okUser, okGroup)
+ val dummyUserResponseInfo: UserResponseInfo = UserResponseInfo(listOfDummyUsers.head,dummyGroup)
"POST groups" should {
"return a 200 response when successful" in {
@@ -705,14 +708,14 @@ class MembershipRoutingSpec
)
doReturn(result(expected))
.when(membershipService)
- .getGroupActivity(anyString, any[Option[String]], anyInt, any[AuthPrincipal])
+ .getGroupActivity(anyString, any[Option[Int]], anyInt, any[AuthPrincipal])
Get(s"/groups/pageSize/activity") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.OK
val maxItemsCaptor = ArgumentCaptor.forClass(classOf[Int])
verify(membershipService).getGroupActivity(
anyString,
- any[Option[String]],
+ any[Option[Int]],
maxItemsCaptor.capture(),
any[AuthPrincipal]
)
@@ -817,9 +820,9 @@ class MembershipRoutingSpec
"GET user" should {
"return a 200 response with the user info" in {
- doReturn(result(okUser))
+ doReturn(result(okUserResponseInfo))
.when(membershipService)
- .getUser("ok", okAuth)
+ .getUserDetails("ok", okAuth)
Get("/users/ok") ~> membershipRoute ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[UserResponseInfo]
@@ -829,9 +832,9 @@ class MembershipRoutingSpec
"return a 200 response with the user info when the user ID is valid" in {
val testUser = listOfDummyUsers.head
- doReturn(result(testUser))
+ doReturn(result(dummyUserResponseInfo))
.when(membershipService)
- .getUser("dummy000", okAuth)
+ .getUserDetails("dummy000", okAuth)
Get("/users/dummy000") ~> membershipRoute ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[UserResponseInfo]
@@ -841,9 +844,9 @@ class MembershipRoutingSpec
"return a 200 response with the user info when the username is valid" in {
val testUser = listOfDummyUsers.head
- doReturn(result(testUser))
+ doReturn(result(dummyUserResponseInfo))
.when(membershipService)
- .getUser("name-dummy000", okAuth)
+ .getUserDetails("name-dummy000", okAuth)
Get("/users/name-dummy000") ~> membershipRoute ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[UserResponseInfo]
@@ -851,10 +854,21 @@ class MembershipRoutingSpec
}
}
+ "return a 200 response with the user info when the group Id is valid" in {
+ doReturn(result(dummyUserResponseInfo))
+ .when(membershipService)
+ .getUserDetails("name-dummy000", okAuth)
+ Get("/users/name-dummy000") ~> membershipRoute ~> check {
+ status shouldBe StatusCodes.OK
+ val result = responseAs[UserResponseInfo]
+ result.groupId shouldBe dummyUserResponseInfo.groupId
+ }
+ }
+
"return a 404 Not Found response when the userIdentifier is not a valid user ID or username" in {
doReturn(result(UserNotFoundError("fail")))
.when(membershipService)
- .getUser("fail", okAuth)
+ .getUserDetails("fail", okAuth)
Get("/users/fail") ~> membershipRoute ~> check {
status shouldBe StatusCodes.NotFound
}
diff --git a/modules/api/src/test/scala/vinyldns/api/route/RecordSetRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/RecordSetRoutingSpec.scala
index ccd70508c..2e997702e 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/RecordSetRoutingSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/RecordSetRoutingSpec.scala
@@ -19,6 +19,7 @@ package vinyldns.api.route
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpRequest, StatusCodes}
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.testkit.ScalatestRouteTest
+
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.json4s.JsonDSL._
@@ -28,7 +29,7 @@ import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import vinyldns.api.Interfaces._
import vinyldns.api.config.LimitsConfig
-import vinyldns.api.domain.record.{ListRecordSetChangesResponse, RecordSetServiceAlgebra}
+import vinyldns.api.domain.record.{ListFailedRecordSetChangesResponse, ListRecordSetChangesResponse, ListRecordSetHistoryResponse, RecordSetServiceAlgebra}
import vinyldns.api.domain.zone._
import vinyldns.core.TestMembershipData.okAuth
import vinyldns.core.domain.Fqdn
@@ -36,6 +37,7 @@ import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
import vinyldns.core.domain.record.RecordType._
+import vinyldns.core.domain.record.RecordTypeSort.{ASC, DESC, RecordTypeSort}
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
@@ -54,6 +56,7 @@ class RecordSetRoutingSpec
private val notAuthorizedZone = Zone("notAuth", "test@test.com")
private val syncingZone = Zone("syncing", "test@test.com")
private val invalidChangeZone = Zone("invalidChange", "test@test.com")
+ private val recordSetCount = RecordSetCount(5)
private val rsAlreadyExists = RecordSet(
okZone.id,
@@ -407,6 +410,19 @@ class RecordSetRoutingSpec
maxItems = 100
)
+ private val changeWithUserName =
+ List(RecordSetChangeInfo(rsChange1, Some("ok")))
+ private val listRecordSetChangeHistoryResponse = ListRecordSetHistoryResponse(
+ Some(okZone.id),
+ changeWithUserName,
+ nextId = None,
+ startFrom = None,
+ maxItems = 100
+ )
+
+ private val failedChangesWithUserName =
+ List(rsChange1.copy(status = RecordSetChangeStatus.Failed) , rsChange2.copy(status = RecordSetChangeStatus.Failed))
+
class TestService extends RecordSetServiceAlgebra {
def evaluate(
@@ -522,7 +538,8 @@ class RecordSetRoutingSpec
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse] = {
if (recordTypeFilter.contains(Set(CNAME))) {
Right(
@@ -564,6 +581,20 @@ class RecordSetRoutingSpec
}
}.toResult
+ def listFailedRecordSetChanges(
+ authPrincipal: AuthPrincipal,
+ zoneId:Option[String],
+ startFrom: Int,
+ maxItems: Int
+ ): Result[ListFailedRecordSetChangesResponse] = {
+ zoneId match {
+ case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
+ case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
+ case _ => authPrincipal match {
+ case _ => Right(ListFailedRecordSetChangesResponse(failedChangesWithUserName,0,startFrom,maxItems))
+ }
+ }}.toResult
+
def searchRecordSets(
startFrom: Option[String],
maxItems: Option[Int],
@@ -571,7 +602,8 @@ class RecordSetRoutingSpec
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListGlobalRecordSetsResponse] = {
if (recordTypeFilter.contains(Set(CNAME))) {
Right(
@@ -621,10 +653,49 @@ class RecordSetRoutingSpec
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
nameSort: NameSort,
- authPrincipal: AuthPrincipal
+ authPrincipal: AuthPrincipal,
+ recordTypeSort: RecordTypeSort
): Result[ListRecordSetsByZoneResponse] = {
zoneId match {
case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
+ // NameSort will be in ASC by default
+ case okZone.id if recordTypeSort==DESC =>
+ Right(
+ ListRecordSetsByZoneResponse(
+ List(
+ RecordSetListInfo(RecordSetInfo(soa, None), AccessLevel.Read),
+ RecordSetListInfo(RecordSetInfo(cname, None), AccessLevel.Read),
+ RecordSetListInfo(RecordSetInfo(aaaa, None), AccessLevel.Read)
+ ),
+ startFrom,
+ None,
+ maxItems,
+ recordNameFilter,
+ recordTypeFilter,
+ None,
+ nameSort,
+ recordTypeSort
+ )
+ )
+ // NameSort will be in ASC by default
+ case okZone.id if recordTypeSort==ASC=>
+ Right(
+ ListRecordSetsByZoneResponse(
+ List(
+ RecordSetListInfo(RecordSetInfo(aaaa, None), AccessLevel.Read),
+ RecordSetListInfo(RecordSetInfo(cname, None), AccessLevel.Read),
+ RecordSetListInfo(RecordSetInfo(soa, None), AccessLevel.Read)
+ ),
+ startFrom,
+ None,
+ maxItems,
+ recordNameFilter,
+ recordTypeFilter,
+ None,
+ nameSort,
+ recordTypeSort
+ )
+ )
case okZone.id if recordTypeFilter.contains(Set(CNAME)) =>
Right(
ListRecordSetsByZoneResponse(
@@ -637,7 +708,8 @@ class RecordSetRoutingSpec
recordNameFilter,
recordTypeFilter,
recordOwnerGroupFilter,
- nameSort
+ nameSort,
+ recordTypeSort=RecordTypeSort.ASC
)
)
case okZone.id if recordTypeFilter.isEmpty =>
@@ -654,7 +726,8 @@ class RecordSetRoutingSpec
recordNameFilter,
recordTypeFilter,
None,
- nameSort
+ nameSort,
+ recordTypeSort=RecordTypeSort.ASC
)
)
}
@@ -673,9 +746,20 @@ class RecordSetRoutingSpec
}
}.toResult
+ def getRecordSetCount(
+ zoneId: String,
+ authPrincipal: AuthPrincipal
+ ): Result[RecordSetCount] = {
+ zoneId match {
+ case zoneNotFound.id => Left(ZoneNotFoundError(s"$zoneId"))
+ case notAuthorizedZone.id => Left(NotAuthorizedError("no way"))
+ case _ => Right(recordSetCount)
+ }
+ }.toResult
+
def listRecordSetChanges(
zoneId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int,
authPrincipal: AuthPrincipal
): Result[ListRecordSetChangesResponse] = {
@@ -686,6 +770,21 @@ class RecordSetRoutingSpec
}
}.toResult
+ def listRecordSetChangeHistory(
+ zoneId: Option[String],
+ startFrom: Option[Int],
+ maxItems: Int,
+ fqdn: Option[String],
+ recordType: Option[RecordType],
+ authPrincipal: AuthPrincipal
+ ): Result[ListRecordSetHistoryResponse] = {
+ zoneId match {
+ case Some(zoneNotFound.id) => Left(ZoneNotFoundError(s"$zoneId"))
+ case Some(notAuthorizedZone.id) => Left(NotAuthorizedError("no way"))
+ case _ => Right(listRecordSetChangeHistoryResponse)
+ }
+ }.toResult
+
}
val recordSetService: RecordSetServiceAlgebra = new TestService
@@ -821,6 +920,46 @@ class RecordSetRoutingSpec
}
}
+ "GET recordset change history" should {
+ "return the recordset change" in {
+ Get(s"/recordsetchange/history?zoneId=${okZone.id}&fqdn=rs1.ok.&recordType=A") ~> recordSetRoute ~> check {
+ val response = responseAs[ListRecordSetHistoryResponse]
+
+ response.zoneId shouldBe Some(okZone.id)
+ (response.recordSetChanges.map(_.id) should contain)
+ .only(rsChange1.id)
+ }
+ }
+
+ "return an error when the record fqdn and type is not defined" in {
+ Get(s"/recordsetchange/history") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.BadRequest
+ }
+ }
+
+ "return a Bad Request when maxItems is out of Bounds" in {
+ Get(s"/recordsetchange/history?maxItems=101") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.BadRequest
+ }
+ Get(s"/recordsetchange/history?maxItems=0") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.BadRequest
+ }
+ }
+ }
+
+ "GET failed record set changes" should {
+ "return the failed record set changes" in {
+ val rsChangeFailed1 = rsChange1.copy(status = RecordSetChangeStatus.Failed)
+ val rsChangeFailed2 = rsChange2.copy(status = RecordSetChangeStatus.Failed)
+
+ Get(s"/metrics/health/zones/${okZone.id}/recordsetchangesfailure") ~> recordSetRoute ~> check {
+ val changes = responseAs[ListFailedRecordSetChangesResponse]
+ changes.failedRecordSetChanges.map(_.id) shouldBe List(rsChangeFailed1.id, rsChangeFailed2.id)
+
+ }
+ }
+ }
+
"GET recordset" should {
"return the recordset summary info" in {
Get(s"/zones/${okZone.id}/recordsets/${rsOk.id}") ~> recordSetRoute ~> check {
@@ -1053,6 +1192,38 @@ class RecordSetRoutingSpec
}
}
+ "return all recordSets types in descending order" in {
+
+ Get(s"/zones/${okZone.id}/recordsets?recordTypeSort=desc") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.OK
+
+ val resultRs = responseAs[ListRecordSetsByZoneResponse]
+ (resultRs.recordSets.map(_.typ) shouldBe List(soa.typ, cname.typ, aaaa.typ))
+ }
+ }
+
+ "return all recordSets types in ascending order" in {
+
+ Get(s"/zones/${okZone.id}/recordsets?recordTypeSort=asc") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.OK
+
+ val resultRs = responseAs[ListRecordSetsByZoneResponse]
+ (resultRs.recordSets.map(_.typ) shouldBe List(aaaa.typ, cname.typ, soa.typ))
+
+ }
+ }
+
+ "return all record name in ascending order when name and type sort simultaneously" in {
+
+ Get(s"/zones/${okZone.id}/recordsets?nameSort=desc&recordTypeSort=asc") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.OK
+
+ val resultRs = responseAs[ListRecordSetsByZoneResponse]
+ (resultRs.recordSets.map(_.name) shouldBe List(aaaa.name, cname.name, soa.name))
+
+ }
+ }
+
"return recordsets of a specific type" in {
Get(s"/zones/${okZone.id}/recordsets?recordTypeFilter=cname") ~> recordSetRoute ~> check {
status shouldBe StatusCodes.OK
@@ -1451,11 +1622,11 @@ class RecordSetRoutingSpec
}
"return errors for invalid NAPTR record data" in {
+ val validFlags = List("U", "S", "A", "P")
validateErrors(
testRecordType(
RecordType.NAPTR,
("replacement" -> Random.alphanumeric.take(260).mkString) ~~
- // should check regex better
("regexp" -> Random.alphanumeric.take(260).mkString) ~~
("service" -> Random.alphanumeric.take(260).mkString) ~~
("flags" -> Random.alphanumeric.take(2).mkString) ~~
@@ -1464,18 +1635,18 @@ class RecordSetRoutingSpec
),
"NAPTR.order must be an unsigned 16 bit number",
"NAPTR.preference must be an unsigned 16 bit number",
- "NAPTR.flags must be less than 2 characters",
+ "Invalid NAPTR.flag. Valid NAPTR flag value must be U, S, A or P",
"NAPTR.service must be less than 255 characters",
- "NAPTR.regexp must be less than 255 characters",
+ "Invalid NAPTR.regexp. Valid NAPTR regexp value must start and end with '!' or can be empty",
"NAPTR.replacement must be less than 255 characters"
)
validateErrors(
testRecordType(
RecordType.NAPTR,
- ("regexp" -> Random.alphanumeric.take(10).mkString) ~~
+ ("regexp" -> "") ~~
("service" -> Random.alphanumeric.take(10).mkString) ~~
("replacement" -> Random.alphanumeric.take(10).mkString) ~~
- ("flags" -> Random.alphanumeric.take(1).mkString) ~~
+ ("flags" -> validFlags.take(1).mkString) ~~
("order" -> -1) ~~
("preference" -> -1)
),
@@ -1535,4 +1706,19 @@ class RecordSetRoutingSpec
)
}
}
+ "GET recordset count by zone" should {
+ "return recordset count" in {
+ Get(s"/zones/${okZone.id}/recordsetcount") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.OK
+ val resultRs = responseAs[RecordSetCount]
+ resultRs shouldBe recordSetCount
+ }
+ }
+
+ "return a 404 Not Found when the zone doesn't exist" in {
+ Get(s"/zones/${zoneNotFound.id}/recordsetcount") ~> recordSetRoute ~> check {
+ status shouldBe StatusCodes.NotFound
+ }
+ }
+ }
}
diff --git a/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala
index 48b2e5910..dd0ce0259 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/VinylDNSJsonProtocolSpec.scala
@@ -26,7 +26,7 @@ import vinyldns.api.VinylDNSTestHelpers
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.{CreateZoneInput, UpdateZoneInput, ZoneConnection}
import vinyldns.core.TestRecordSetData._
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.Messages._
class VinylDNSJsonProtocolSpec
@@ -43,7 +43,7 @@ class VinylDNSJsonProtocolSpec
ZoneConnection(
"primaryConnection",
"primaryConnectionKeyName",
- "primaryConnectionKey",
+ Encrypted("primaryConnectionKey"),
"10.1.1.1"
)
),
@@ -51,7 +51,7 @@ class VinylDNSJsonProtocolSpec
ZoneConnection(
"transferConnection",
"transferConnectionKeyName",
- "transferConnectionKey",
+ Encrypted("transferConnectionKey"),
"10.1.1.2"
)
),
@@ -66,7 +66,7 @@ class VinylDNSJsonProtocolSpec
ZoneConnection(
"primaryConnection",
"primaryConnectionKeyName",
- "primaryConnectionKey",
+ Encrypted("primaryConnectionKey"),
"10.1.1.1"
)
),
@@ -74,7 +74,7 @@ class VinylDNSJsonProtocolSpec
ZoneConnection(
"transferConnection",
"transferConnectionKeyName",
- "transferConnectionKey",
+ Encrypted("transferConnectionKey"),
"10.1.1.2"
)
),
@@ -312,6 +312,25 @@ class VinylDNSJsonProtocolSpec
}
}
+ "EncryptedSerializer" should {
+ "parse a secret key to Encrypted type" in {
+ val secretKeyInput: JValue = "primaryConnectionKey"
+
+ val actual = secretKeyInput.extract[Encrypted]
+ val expected = Encrypted("primaryConnectionKey")
+ actual shouldBe expected
+ }
+
+ "throw an error if there is a type mismatch during deserialization" in {
+ val secretKeyInput: JObject =
+ "key" -> List(
+ "primaryConnectionKey"
+ )
+
+ assertThrows[MappingException](secretKeyInput.extract[Encrypted])
+ }
+ }
+
"RecordSetSerializer" should {
"parse a record set with an absolute CNAME record passes" in {
val recordSetJValue: JValue =
@@ -661,6 +680,206 @@ class VinylDNSJsonProtocolSpec
val thrown = the[MappingException] thrownBy recordSetJValue.extract[RecordSet]
thrown.msg should include("Digest Type 0 is not a supported DS record digest type")
}
+ "auto-approve a owner ship transfer request" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("recordSetGroupChange" -> Some("ownerShipTransferStatus" -> "AutoApproved"))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.AutoApproved))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.AutoApproved
+ }
+
+ "manually-approve a owner ship transfer request" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("ownerGroupId" -> "updated-admin-group-id") ~~
+ ("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "ManuallyApproved")~~
+ ("requestedOwnerGroupId" -> "updated-admin-group-id")))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ ownerGroupId = Some("updated-admin-group-id"),
+ recordSetGroupChange = Some(OwnerShipTransfer(ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyApproved,requestedOwnerGroupId = Some("updated-admin-group-id")))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyApproved
+ anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
+ anonymize(actual).ownerGroupId shouldBe Some("updated-admin-group-id")
+ }
+
+ "request a owner ship transfer" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("ownerGroupId" -> "updated-ok-group-id") ~~
+ ("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "Requested")~~
+ ("requestedOwnerGroupId" -> "updated-admin-group-id")))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ ownerGroupId = Some("updated-ok-group-id"),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Requested,
+ requestedOwnerGroupId = Some("updated-admin-group-id")))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Requested
+ anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
+ anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
+ }
+
+ "request a owner ship transfer which will change to pending review" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("ownerGroupId" -> "updated-ok-group-id") ~~
+ ("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "PendingReview")~~
+ ("requestedOwnerGroupId" -> "updated-admin-group-id")))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ ownerGroupId = Some("updated-ok-group-id"),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.PendingReview,
+ requestedOwnerGroupId = Some("updated-admin-group-id")))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.PendingReview
+ anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
+ anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
+ }
+
+ "cancel a owner ship transfer request" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("ownerGroupId" -> "updated-ok-group-id") ~~
+ ("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "Cancelled")~~
+ ("requestedOwnerGroupId" -> "updated-admin-group-id")))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ ownerGroupId = Some("updated-ok-group-id"),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.Cancelled,
+ requestedOwnerGroupId = Some("updated-admin-group-id")))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.Cancelled
+ anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
+ anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
+ }
+
+ "manually-reject a owner ship transfer request" in {
+ val recordSetJValue: JValue =
+ ("zoneId" -> "1") ~~
+ ("name" -> "TestRecordName") ~~
+ ("type" -> "CNAME") ~~
+ ("ttl" -> 1000) ~~
+ ("status" -> "Pending") ~~
+ ("records" -> List("cname" -> "cname.data ")) ~~
+ ("ownerGroupId" -> "updated-ok-group-id") ~~
+ ("recordSetGroupChange" -> Some(("ownerShipTransferStatus" -> "ManuallyRejected")~~
+ ("requestedOwnerGroupId" -> "updated-admin-group-id")))
+
+
+ val expected = RecordSet(
+ "1",
+ "TestRecordName",
+ RecordType.CNAME,
+ 1000,
+ RecordSetStatus.Pending,
+ LocalDateTime.of(2010, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC),
+ records = List(CNAMEData(Fqdn("cname.data."))),
+ ownerGroupId = Some("updated-ok-group-id"),
+ recordSetGroupChange = Some(OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.ManuallyRejected,
+ requestedOwnerGroupId = Some("updated-admin-group-id")))
+
+ )
+
+ val actual = recordSetJValue.extract[RecordSet]
+ anonymize(actual) shouldBe anonymize(expected)
+ anonymize(actual).recordSetGroupChange.get.ownerShipTransferStatus shouldBe OwnerShipTransferStatus.ManuallyRejected
+ anonymize(actual).recordSetGroupChange.get.requestedOwnerGroupId shouldBe Some("updated-admin-group-id")
+ anonymize(actual).ownerGroupId shouldBe Some("updated-ok-group-id")
+ }
}
}
diff --git a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala
index f3750a5b6..e3b15da57 100644
--- a/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala
+++ b/modules/api/src/test/scala/vinyldns/api/route/ZoneRoutingSpec.scala
@@ -33,6 +33,7 @@ import vinyldns.api.domain.zone.{ZoneServiceAlgebra, _}
import vinyldns.core.TestMembershipData._
import vinyldns.core.TestZoneData._
import vinyldns.core.crypto.{JavaCrypto, NoOpCrypto}
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.domain.record.RecordType
import vinyldns.core.domain.zone._
@@ -81,23 +82,24 @@ class ZoneRoutingSpec
private val ok = Zone("ok.", "ok@test.com", acl = zoneAcl, adminGroupId = "test")
private val aclAsInfo = ZoneACLInfo(zoneAcl.rules.map(ACLRuleInfo(_, Some("name"))))
private val okAsZoneInfo = ZoneInfo(ok, aclAsInfo, okGroup.name, AccessLevel.Read)
+ private val okAsZoneDetails = ZoneDetails(ok, okGroup.name)
private val badRegex = Zone("ok.", "bad-regex@test.com", adminGroupId = "test")
private val trailingDot = Zone("trailing.dot", "trailing-dot@test.com")
private val connectionOk = Zone(
"connection.ok.",
"connection-ok@test.com",
- connection = Some(ZoneConnection("connection.ok", "keyName", "key", "10.1.1.1")),
- transferConnection = Some(ZoneConnection("connection.ok", "keyName", "key", "10.1.1.1"))
+ connection = Some(ZoneConnection("connection.ok", "keyName", Encrypted("key"), "10.1.1.1")),
+ transferConnection = Some(ZoneConnection("connection.ok", "keyName", Encrypted("key"), "10.1.1.1"))
)
private val connectionFailed = Zone(
"connection.fail",
"connection-failed@test.com",
- connection = Some(ZoneConnection("connection.fail", "keyName", "key", "10.1.1.1"))
+ connection = Some(ZoneConnection("connection.fail", "keyName", Encrypted("key"), "10.1.1.1"))
)
private val zoneValidationFailed = Zone(
"validation.fail",
"zone-validation-failed@test.com",
- connection = Some(ZoneConnection("validation.fail", "keyName", "key", "10.1.1.1"))
+ connection = Some(ZoneConnection("validation.fail", "keyName", Encrypted("key"), "10.1.1.1"))
)
private val zone1 = Zone("zone1.", "zone1@test.com", ZoneStatus.Active)
private val zoneSummaryInfo1 = ZoneSummaryInfo(zone1, okGroup.name, AccessLevel.NoAccess)
@@ -111,7 +113,30 @@ class ZoneRoutingSpec
private val zone5 =
Zone("zone5.", "zone5@test.com", ZoneStatus.Active, adminGroupId = xyzGroup.id)
private val zoneSummaryInfo5 = ZoneSummaryInfo(zone5, xyzGroup.name, AccessLevel.NoAccess)
+ private val zone6 =
+ Zone("zone6.in-addr.arpa.", "zone6@test.com", ZoneStatus.Active, adminGroupId = xyzGroup.id)
+ private val zoneSummaryInfo6 = ZoneSummaryInfo(zone6, xyzGroup.name, AccessLevel.NoAccess)
private val error = Zone("error.", "error@test.com")
+ private val deletedZone1 = Zone("ok1.", "ok1@test.com", ZoneStatus.Deleted , acl = zoneAcl)
+ private val deletedZoneChange1 = ZoneChange(deletedZone1, "ok1", ZoneChangeType.Create, ZoneChangeStatus.Synced)
+ private val ZoneChangeDeletedInfo1 = ZoneChangeDeletedInfo(
+ deletedZoneChange1, okGroup.name, okUser.userName, AccessLevel.NoAccess)
+ private val deletedZone2 = Zone("ok2.", "ok2@test.com", ZoneStatus.Deleted , acl = zoneAcl)
+ private val deletedZoneChange2 = ZoneChange(deletedZone2, "ok2", ZoneChangeType.Create, ZoneChangeStatus.Synced)
+ private val ZoneChangeDeletedInfo2 = ZoneChangeDeletedInfo(
+ deletedZoneChange2, okGroup.name, okUser.userName, AccessLevel.NoAccess)
+ private val deletedZone3 = Zone("ok3.", "ok3@test.com", ZoneStatus.Deleted , acl = zoneAcl)
+ private val deletedZoneChange3 = ZoneChange(deletedZone3, "ok3", ZoneChangeType.Create, ZoneChangeStatus.Synced)
+ private val ZoneChangeDeletedInfo3= ZoneChangeDeletedInfo(
+ deletedZoneChange3, okGroup.name, okUser.userName, AccessLevel.NoAccess)
+ private val deletedZone4 = Zone("ok4.", "ok4@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id)
+ private val deletedZoneChange4 = ZoneChange(deletedZone4, "ok4", ZoneChangeType.Create, ZoneChangeStatus.Synced)
+ private val ZoneChangeDeletedInfo4 = ZoneChangeDeletedInfo(
+ deletedZoneChange4, okGroup.name, okUser.userName, AccessLevel.NoAccess)
+ private val deletedZone5 = Zone("ok5.", "ok5@test.com", ZoneStatus.Deleted , acl = zoneAcl, adminGroupId = xyzGroup.id)
+ private val deletedZoneChange5 = ZoneChange(deletedZone5, "ok5", ZoneChangeType.Create, ZoneChangeStatus.Synced)
+ private val ZoneChangeDeletedInfo5 = ZoneChangeDeletedInfo(
+ deletedZoneChange5, okGroup.name, okUser.userName, AccessLevel.NoAccess)
private val missingFields: JValue =
("invalidField" -> "randomValue") ~~
@@ -133,6 +158,13 @@ class ZoneRoutingSpec
maxItems = 100
)
+ private val listFailedZoneChangeResponse = ListFailedZoneChangesResponse(
+ List(zoneCreate.copy(status=ZoneChangeStatus.Failed), zoneUpdate.copy(status=ZoneChangeStatus.Failed)),
+ nextId = 0,
+ startFrom = 0,
+ maxItems = 100
+ )
+
val crypto = new JavaCrypto(
ConfigFactory.parseString(
"""secret = "8B06A7F3BC8A2497736F1916A123AA40E88217BE9264D8872597EF7A6E5DCE61""""
@@ -238,6 +270,15 @@ class ZoneRoutingSpec
outcome.toResult
}
+ def getCommonZoneDetails(zoneId: String, auth: AuthPrincipal): Result[ZoneDetails] = {
+ val outcome = zoneId match {
+ case notFound.id => Left(ZoneNotFoundError(s"$zoneId"))
+ case ok.id => Right(okAsZoneDetails)
+ case error.id => Left(new RuntimeException("fail"))
+ }
+ outcome.toResult
+ }
+
def getZoneByName(zoneName: String, auth: AuthPrincipal): Result[ZoneInfo] = {
val outcome = zoneName match {
case notFound.name => Left(ZoneNotFoundError(s"$zoneName"))
@@ -253,11 +294,12 @@ class ZoneRoutingSpec
startFrom: Option[String],
maxItems: Int,
searchByAdminGroup: Boolean = false,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): Result[ListZonesResponse] = {
- val outcome = (authPrincipal, nameFilter, startFrom, maxItems, ignoreAccess) match {
- case (_, None, Some("zone3."), 3, false) =>
+ val outcome = (authPrincipal, nameFilter, startFrom, maxItems, ignoreAccess, includeReverse) match {
+ case (_, None, Some("zone3."), 3, false, true) =>
Right(
ListZonesResponse(
zones = List(zoneSummaryInfo1, zoneSummaryInfo2, zoneSummaryInfo3),
@@ -268,7 +310,7 @@ class ZoneRoutingSpec
ignoreAccess = false
)
)
- case (_, None, Some("zone4."), 4, false) =>
+ case (_, None, Some("zone4."), 4, false, true) =>
Right(
ListZonesResponse(
zones = List(zoneSummaryInfo1, zoneSummaryInfo2, zoneSummaryInfo3),
@@ -280,7 +322,7 @@ class ZoneRoutingSpec
)
)
- case (_, None, None, 3, false) =>
+ case (_, None, None, 3, false, true) =>
Right(
ListZonesResponse(
zones = List(zoneSummaryInfo1, zoneSummaryInfo2, zoneSummaryInfo3),
@@ -292,7 +334,7 @@ class ZoneRoutingSpec
)
)
- case (_, None, None, 5, true) =>
+ case (_, None, None, 6, true, true) =>
Right(
ListZonesResponse(
zones = List(
@@ -300,17 +342,38 @@ class ZoneRoutingSpec
zoneSummaryInfo2,
zoneSummaryInfo3,
zoneSummaryInfo4,
- zoneSummaryInfo5
+ zoneSummaryInfo5,
+ zoneSummaryInfo6
),
nameFilter = None,
startFrom = None,
nextId = None,
- maxItems = 5,
- ignoreAccess = true
+ maxItems = 6,
+ ignoreAccess = true,
+ includeReverse = true
)
)
- case (_, Some(filter), Some("zone4."), 4, false) =>
+ case (_, None, None, 6, true, false) =>
+ Right(
+ ListZonesResponse(
+ zones = List(
+ zoneSummaryInfo1,
+ zoneSummaryInfo2,
+ zoneSummaryInfo3,
+ zoneSummaryInfo4,
+ zoneSummaryInfo5,
+ ),
+ nameFilter = None,
+ startFrom = None,
+ nextId = None,
+ maxItems = 6,
+ ignoreAccess = true,
+ includeReverse = false
+ )
+ )
+
+ case (_, Some(filter), Some("zone4."), 4, false, true) =>
Right(
ListZonesResponse(
zones = List(zoneSummaryInfo1, zoneSummaryInfo2, zoneSummaryInfo3),
@@ -322,7 +385,20 @@ class ZoneRoutingSpec
)
)
- case (_, None, None, _, _) =>
+ case (_, Some(filter), Some("zone4."), 4, true, false) =>
+ Right(
+ ListZonesResponse(
+ zones = List(zoneSummaryInfo4, zoneSummaryInfo5),
+ nameFilter = Some(filter),
+ startFrom = Some("zone4."),
+ nextId = None,
+ maxItems = 4,
+ ignoreAccess = true,
+ includeReverse = false
+ )
+ )
+
+ case (_, None, None, _, _, true) =>
Right(
ListZonesResponse(
zones = List(zoneSummaryInfo1, zoneSummaryInfo2, zoneSummaryInfo3),
@@ -339,6 +415,92 @@ class ZoneRoutingSpec
outcome.toResult
}
+ def listDeletedZones(
+ authPrincipal: AuthPrincipal,
+ nameFilter: Option[String],
+ startFrom: Option[String],
+ maxItems: Int,
+ ignoreAccess: Boolean = false
+ ): Result[ListDeletedZoneChangesResponse] = {
+
+ val outcome = (authPrincipal, nameFilter, startFrom, maxItems, ignoreAccess) match {
+ case (_, None, Some("zone3."), 3, false) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
+ zoneChangeFilter = None,
+ startFrom = Some("zone3."),
+ nextId = Some("zone6."),
+ maxItems = 3,
+ ignoreAccess = false
+ )
+ )
+ case (_, None, Some("zone4."), 4, false) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
+ zoneChangeFilter = None,
+ startFrom = Some("zone4."),
+ nextId = None,
+ maxItems = 4,
+ ignoreAccess = false
+ )
+ )
+
+ case (_, None, None, 3, false) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
+ zoneChangeFilter = None,
+ startFrom = None,
+ nextId = Some("zone3."),
+ maxItems = 3,
+ ignoreAccess = false
+ )
+ )
+
+ case (_, None, None, 5, true) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo =
+ List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3, ZoneChangeDeletedInfo4,ZoneChangeDeletedInfo5),
+ zoneChangeFilter = None,
+ startFrom = None,
+ nextId = None,
+ maxItems = 5,
+ ignoreAccess = true
+ )
+ )
+
+ case (_, Some(filter), Some("zone4."), 4, false) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
+ zoneChangeFilter = Some(filter),
+ startFrom = Some("zone4."),
+ nextId = None,
+ maxItems = 4,
+ ignoreAccess = false
+ )
+ )
+
+ case (_, None, None, _, _) =>
+ Right(
+ ListDeletedZoneChangesResponse(
+ zonesDeletedInfo = List(ZoneChangeDeletedInfo1,ZoneChangeDeletedInfo2,ZoneChangeDeletedInfo3),
+ zoneChangeFilter = None,
+ startFrom = None,
+ nextId = None,
+ ignoreAccess = false
+ )
+ )
+
+ case _ => Left(InvalidRequest("shouldnt get here"))
+ }
+
+ outcome.toResult
+ }
+
def listZoneChanges(
zoneId: String,
authPrincipal: AuthPrincipal,
@@ -353,6 +515,17 @@ class ZoneRoutingSpec
outcome.toResult
}
+ def listFailedZoneChanges(
+ authPrincipal: AuthPrincipal,
+ startFrom: Int,
+ maxItems: Int
+ ): Result[ListFailedZoneChangesResponse] = {
+ val outcome = authPrincipal match {
+ case _ => Right(listFailedZoneChangeResponse)
+ }
+ outcome.toResult
+ }
+
def addACLRule(
zoneId: String,
aclRuleInfo: ACLRuleInfo,
@@ -665,10 +838,10 @@ class ZoneRoutingSpec
val resultKey = result.zone.connection.get.key
val resultTCKey = result.zone.transferConnection.get.key
- val decrypted = crypto.decrypt(resultKey)
- val decryptedTC = crypto.decrypt(resultTCKey)
- decrypted shouldBe connectionOk.connection.get.key
- decryptedTC shouldBe connectionOk.transferConnection.get.key
+ val decrypted = crypto.decrypt(resultKey.value)
+ val decryptedTC = crypto.decrypt(resultTCKey.value)
+ decrypted shouldBe connectionOk.connection.get.key.value
+ decryptedTC shouldBe connectionOk.transferConnection.get.key.value
}
}
@@ -833,6 +1006,27 @@ class ZoneRoutingSpec
}
}
+ "GET zone details" should {
+ "return the zone is retrieved" in {
+ Get(s"/zones/${ok.id}/details") ~> zoneRoute ~> check {
+ status shouldBe OK
+
+ val resultZone = responseAs[GetZoneDetailsResponse].zone
+ resultZone.email shouldBe ok.email
+ resultZone.name shouldBe ok.name
+ Option(resultZone.status) shouldBe defined
+ resultZone.adminGroupId shouldBe "test"
+ resultZone.adminGroupName shouldBe "ok"
+ }
+ }
+
+ "return 404 if the zone does not exist" in {
+ Get(s"/zones/${notFound.id}/details") ~> zoneRoute ~> check {
+ status shouldBe NotFound
+ }
+ }
+ }
+
"GET zone by name " should {
"return the zone is retrieved" in {
Get(s"/zones/name/${ok.name}") ~> zoneRoute ~> check {
@@ -935,17 +1129,48 @@ class ZoneRoutingSpec
}
}
+ "return zones by admin group name when searchByAdminGroup is true and includeReverse is false" in {
+ Get(s"/zones?nameFilter=xyz&startFrom=zone4.&maxItems=4&searchByAdminGroup=true&ignoreAccess=true&includeReverse=false") ~> zoneRoute ~> check {
+ val resp = responseAs[ListZonesResponse]
+ val zones = resp.zones
+ (zones.map(_.id) should contain)
+ .only(zone4.id, zone5.id)
+ resp.nextId shouldBe None
+ resp.maxItems shouldBe 4
+ resp.startFrom shouldBe Some("zone4.")
+ resp.nameFilter shouldBe Some("xyz")
+ resp.ignoreAccess shouldBe true
+ resp.includeReverse shouldBe false
+ }
+ }
+
"return all zones when list all is true" in {
- Get(s"/zones?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
+ Get(s"/zones?maxItems=6&ignoreAccess=true") ~> zoneRoute ~> check {
+ val resp = responseAs[ListZonesResponse]
+ val zones = resp.zones
+ (zones.map(_.id) should contain)
+ .only(zone1.id, zone2.id, zone3.id, zone4.id, zone5.id, zone6.id)
+ resp.nextId shouldBe None
+ resp.maxItems shouldBe 6
+ resp.startFrom shouldBe None
+ resp.nameFilter shouldBe None
+ resp.ignoreAccess shouldBe true
+ resp.includeReverse shouldBe true
+ }
+ }
+
+ "return all forward zones when list all is true and includeReverse is false" in {
+ Get(s"/zones?maxItems=6&ignoreAccess=true&includeReverse=false") ~> zoneRoute ~> check {
val resp = responseAs[ListZonesResponse]
val zones = resp.zones
(zones.map(_.id) should contain)
.only(zone1.id, zone2.id, zone3.id, zone4.id, zone5.id)
resp.nextId shouldBe None
- resp.maxItems shouldBe 5
+ resp.maxItems shouldBe 6
resp.startFrom shouldBe None
resp.nameFilter shouldBe None
resp.ignoreAccess shouldBe true
+ resp.includeReverse shouldBe false
}
}
@@ -959,6 +1184,83 @@ class ZoneRoutingSpec
}
}
+ "GET Deleted zones" should {
+ "return the next id when more results exist" in {
+ Get(s"/zones/deleted/changes?startFrom=zone3.&maxItems=3") ~> zoneRoute ~> check {
+ val resp = responseAs[ListDeletedZoneChangesResponse]
+ val deletedZones = resp.zonesDeletedInfo
+ (deletedZones.map(_.zoneChange.zone.id) should contain)
+ .only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
+ resp.nextId shouldBe Some("zone6.")
+ resp.maxItems shouldBe 3
+ resp.startFrom shouldBe Some("zone3.")
+ }
+ }
+
+ "not return the next id when there are no more results" in {
+ Get(s"/zones/deleted/changes?startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check {
+ val resp = responseAs[ListDeletedZoneChangesResponse]
+ val deletedZones = resp.zonesDeletedInfo
+ (deletedZones.map(_.zoneChange.zone.id) should contain)
+ .only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
+ resp.nextId shouldBe None
+ resp.maxItems shouldBe 4
+ resp.startFrom shouldBe Some("zone4.")
+ resp.ignoreAccess shouldBe false
+ }
+ }
+
+ "not return the start from when not provided" in {
+ Get(s"/zones/deleted/changes?maxItems=3") ~> zoneRoute ~> check {
+ val resp = responseAs[ListDeletedZoneChangesResponse]
+ val deletedZones = resp.zonesDeletedInfo
+ (deletedZones.map(_.zoneChange.zone.id) should contain)
+ .only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
+ resp.nextId shouldBe Some("zone3.")
+ resp.maxItems shouldBe 3
+ resp.startFrom shouldBe None
+ resp.ignoreAccess shouldBe false
+ }
+ }
+
+ "return the name filter when provided" in {
+ Get(s"/zones/deleted/changes?nameFilter=foo&startFrom=zone4.&maxItems=4") ~> zoneRoute ~> check {
+ val resp = responseAs[ListDeletedZoneChangesResponse]
+ val deletedZones = resp.zonesDeletedInfo
+ (deletedZones.map(_.zoneChange.zone.id) should contain)
+ .only(deletedZone1.id, deletedZone2.id, deletedZone3.id)
+ resp.nextId shouldBe None
+ resp.maxItems shouldBe 4
+ resp.startFrom shouldBe Some("zone4.")
+ resp.zoneChangeFilter shouldBe Some("foo")
+ resp.ignoreAccess shouldBe false
+ }
+ }
+
+ "return all zones when list all is true" in {
+ Get(s"/zones/deleted/changes?maxItems=5&ignoreAccess=true") ~> zoneRoute ~> check {
+ val resp = responseAs[ListDeletedZoneChangesResponse]
+ val deletedZones = resp.zonesDeletedInfo
+ (deletedZones.map(_.zoneChange.zone.id) should contain)
+ .only(deletedZone1.id, deletedZone2.id, deletedZone3.id, deletedZone4.id, deletedZone5.id)
+ resp.nextId shouldBe None
+ resp.maxItems shouldBe 5
+ resp.startFrom shouldBe None
+ resp.zoneChangeFilter shouldBe None
+ resp.ignoreAccess shouldBe true
+ }
+ }
+
+ "return an error if the max items is out of range" in {
+ Get(s"/zones/deleted/changes?maxItems=700") ~> zoneRoute ~> check {
+ status shouldBe BadRequest
+ responseEntity.toString should include(
+ "maxItems was 700, maxItems must be between 0 and 100"
+ )
+ }
+ }
+ }
+
"GET zone changes" should {
"return the zone changes" in {
Get(s"/zones/${ok.id}/changes") ~> zoneRoute ~> check {
@@ -992,6 +1294,18 @@ class ZoneRoutingSpec
}
}
+ "GET failed zone changes" should {
+ "return the failed zone changes" in {
+ val zoneCreateFailed = zoneCreate.copy(status = ZoneChangeStatus.Failed)
+ val zoneUpdateFailed = zoneUpdate.copy(status = ZoneChangeStatus.Failed)
+ Get(s"/metrics/health/zonechangesfailure") ~> zoneRoute ~> check {
+ val changes = responseAs[ListFailedZoneChangesResponse]
+ changes.failedZoneChanges.map(_.id) shouldBe List(zoneCreateFailed.id, zoneUpdateFailed.id)
+
+ }
+ }
+ }
+
"PUT zone" should {
"return 202 when the zone is updated" in {
Put(s"/zones/${ok.id}")
diff --git a/modules/api/src/universal/conf/application.conf b/modules/api/src/universal/conf/application.conf
index a8aa7175c..56008f0a7 100644
--- a/modules/api/src/universal/conf/application.conf
+++ b/modules/api/src/universal/conf/application.conf
@@ -27,10 +27,16 @@ vinyldns {
scheduled-changes-enabled = ${?SCHEDULED_CHANGES_ENABLED}
multi-record-batch-change-enabled = true
multi-record-batch-change-enabled = ${?MULTI_RECORD_BATCH_CHANGE_ENABLED}
+
+ # Server settings
use-recordset-cache = false
use-recordset-cache = ${?USE_RECORDSET_CACHE}
load-test-data = false
load-test-data = ${?LOAD_TEST_DATA}
+ # should be true while running locally or when we have only one api server/instance, for zone sync scheduler to work
+ is-zone-sync-schedule-allowed = true
+ # should be set to true only on a single server/instance else automated sync will be performed at every server/instance
+ is-zone-sync-schedule-allowed = ${?IS_ZONE_SYNC_SCHEDULE_ALLOWED}
# configured backend providers
backend {
@@ -155,6 +161,18 @@ vinyldns {
port=${?API_SERVICE_PORT}
}
+ api {
+ limits {
+ batchchange-routing-max-items-limit = 100
+ membership-routing-default-max-items = 100
+ membership-routing-max-items-limit = 1000
+ membership-routing-max-groups-list-limit = 3000
+ recordset-routing-default-max-items= 100
+ zone-routing-default-max-items = 100
+ zone-routing-max-items-limit = 100
+ }
+ }
+
approved-name-servers = [
"172.17.42.1.",
@@ -182,9 +200,9 @@ vinyldns {
name = ${?DATABASE_NAME}
driver = "org.mariadb.jdbc.Driver"
driver = ${?JDBC_DRIVER}
- migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass"
+ migration-url = "jdbc:mariadb://localhost:19002/?user=root&password=pass&socketTimeout=20000"
migration-url = ${?JDBC_MIGRATION_URL}
- url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass"
+ url = "jdbc:mariadb://localhost:19002/vinyldns?user=root&password=pass&socketTimeout=20000"
url = ${?JDBC_URL}
user = "root"
user = ${?JDBC_USER}
@@ -192,6 +210,12 @@ vinyldns {
password = ${?JDBC_PASSWORD}
flyway-out-of-order = false
flyway-out-of-order = ${?FLYWAY_OUT_OF_ORDER}
+
+ max-lifetime = 300000
+ connection-timeout-millis = 30000
+ idle-timeout = 150000
+ maximum-pool-size = 20
+ minimum-idle = 5
}
# TODO: Remove the need for these useless configuration blocks
@@ -328,6 +352,10 @@ akka.http {
# Set to `infinite` to disable.
bind-timeout = 5s
+ # A default request timeout is applied globally to all routes and can be configured using the
+ # akka.http.server.request-timeout setting (which defaults to 20 seconds).
+ # request-timeout = 60s
+
# Show verbose error messages back to the client
verbose-error-messages = on
}
diff --git a/modules/api/src/universal/conf/logback.xml b/modules/api/src/universal/conf/logback.xml
index 4f55b5431..4e4f1f6aa 100644
--- a/modules/api/src/universal/conf/logback.xml
+++ b/modules/api/src/universal/conf/logback.xml
@@ -7,6 +7,10 @@
+
+
+
+
diff --git a/modules/core/src/main/protobuf/VinylDNSProto.proto b/modules/core/src/main/protobuf/VinylDNSProto.proto
index 25f3dee3f..4d710aa24 100644
--- a/modules/core/src/main/protobuf/VinylDNSProto.proto
+++ b/modules/core/src/main/protobuf/VinylDNSProto.proto
@@ -49,6 +49,8 @@ message Zone {
optional int64 latestSync = 13;
optional bool isTest = 14 [default = false];
optional string backendId = 15;
+ optional string recurrenceSchedule = 16;
+ optional string scheduleRequestor = 17;
}
message AData {
@@ -139,7 +141,13 @@ message RecordSet {
repeated RecordData record = 9;
required string account = 10;
optional string ownerGroupId = 11;
- optional string fqdn = 12;
+ optional ownerShipTransfer recordSetGroupChange = 12;
+ optional string fqdn = 13;
+}
+
+message ownerShipTransfer {
+ optional string requestedOwnerGroupId = 1;
+ optional string ownerShipTransferStatus = 2;
}
message RecordSetChange {
diff --git a/modules/core/src/main/scala/vinyldns/core/Messages.scala b/modules/core/src/main/scala/vinyldns/core/Messages.scala
index 0c6929f4f..95c61793e 100644
--- a/modules/core/src/main/scala/vinyldns/core/Messages.scala
+++ b/modules/core/src/main/scala/vinyldns/core/Messages.scala
@@ -64,9 +64,6 @@ object Messages {
// Error displayed when NSData field is not a positive integer
val NSDataError = "NS data must be a positive integer"
- // Error displayed when importing files other than .csv
- val ImportError = "Import failed. Not a valid file. File should be of ‘.csv’ type."
-
/*
* Error displayed when user is not authorized to make changes to the record
*
@@ -81,4 +78,14 @@ object Messages {
// Error displayed when group name or email is empty
val GroupValidationErrorMsg = "Group name and email cannot be empty."
+
+ val EmailValidationErrorMsg = "Please enter a valid Email. Valid domains should end with"
+
+ val InvalidEmailValidationErrorMsg = "Please enter a valid Email."
+
+ val DotsValidationErrorMsg = "Please enter a valid Email. Number of dots allowed after @ is"
+
+ val nonExistentRecordDeleteMessage = "This record does not exist. No further action is required."
+
+ val nonExistentRecordDataDeleteMessage = "Record data entered does not exist. No further action is required."
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala b/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala
index 1fe3ab85a..4d68bf43d 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/DomainValidationErrors.scala
@@ -52,6 +52,11 @@ final case class InvalidDomainName(param: String) extends DomainValidationError
"joined by dots, and terminated with a dot."
}
+final case class InvalidIPv4CName(param: String) extends DomainValidationError {
+ def message: String =
+ s"""Invalid Cname: "$param", Valid CNAME record data should not be an IP address"""
+}
+
final case class InvalidCname(param: String, isReverseZone: Boolean) extends DomainValidationError {
def message: String =
isReverseZone match {
@@ -102,10 +107,22 @@ final case class InvalidTTL(param: Long, min: Long, max: Long) extends DomainVal
s"""Invalid TTL: "${param.toString}", must be a number between $min and $max."""
}
-final case class InvalidMxPreference(param: Long, min: Long, max: Long)
+final case class InvalidMX_NAPTR_SRVData(param: Long, min: Long, max: Long, recordDataType: String, recordType: String)
extends DomainValidationError {
def message: String =
- s"""Invalid MX Preference: "${param.toString}", must be a number between $min and $max."""
+ s"""Invalid $recordType $recordDataType: "${param.toString}", must be a number between $min and $max."""
+}
+
+final case class InvalidNaptrFlag(value: String)
+ extends DomainValidationError {
+ def message: String =
+ s"""Invalid NAPTR flag value: '$value'. Valid NAPTR flag value must be U, S, A or P."""
+}
+
+final case class InvalidNaptrRegexp(value: String)
+ extends DomainValidationError {
+ def message: String =
+ s"""Invalid NAPTR regexp value: '$value'. Valid NAPTR regexp value must start and end with '!'."""
}
final case class InvalidBatchRecordType(param: String, supported: Set[RecordType])
@@ -121,15 +138,10 @@ final case class ZoneDiscoveryError(name: String, fatal: Boolean = false)
"If zone exists, then it must be connected to in VinylDNS."
}
-final case class RecordAlreadyExists(name: String, recordData: RecordData, isApproved:Boolean,
- fatal: Boolean = false) extends DomainValidationError(fatal) {
- def message: String = {
- if (isApproved == false)
- s"""RecordName "$name" already exists. Your request will be manually reviewed. """ +
- "If you intended to update this record, you can avoid manual review by adding " +
- " a DeleteRecordSet entry followed by an Add."
- else s"""ℹ️ Record data "$recordData" is does not exists.
- Complete the request in DNS and give approve. """ }
+final case class RecordAlreadyExists(name: String) extends DomainValidationError {
+ def message: String =
+ s"""RecordName "$name" already exists. """ +
+ "If you intended to update this record, submit a DeleteRecordSet entry followed by an Add."
}
final case class RecordDoesNotExist(name: String) extends DomainValidationError {
@@ -137,6 +149,12 @@ final case class RecordDoesNotExist(name: String) extends DomainValidationError
s"""Record "$name" Does Not Exist: cannot delete a record that does not exist."""
}
+final case class InvalidUpdateRequest(name: String) extends DomainValidationError {
+ def message: String =
+ s"""Cannot perform request for the record "$name". """ +
+ "Add and Delete for the record with same record data exists in the batch."
+}
+
final case class CnameIsNotUniqueError(name: String, typ: RecordType)
extends DomainValidationError {
def message: String =
@@ -194,6 +212,17 @@ final case class RecordRequiresManualReview(fqdn: String, fatal: Boolean = false
.replaceAll("\n", " ")
}
+final case class InvalidBatchRequest(msg: String) extends DomainValidationError {
+ def message: String =
+ s"""$msg"""
+ .replaceAll("\n", " ")
+}
+
+final case class NotApprovedNSError(nsData: String) extends DomainValidationError {
+ def message: String =
+ s"Name Server $nsData is not an approved name server."
+}
+
final case class UnsupportedOperation(operation: String) extends DomainValidationError {
def message: String = s"$operation is not yet implemented/supported in VinylDNS."
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/Encryption.scala b/modules/core/src/main/scala/vinyldns/core/domain/Encryption.scala
new file mode 100644
index 000000000..f9c851094
--- /dev/null
+++ b/modules/core/src/main/scala/vinyldns/core/domain/Encryption.scala
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 Comcast Cable Communications Management, LLC
+ *
+ * 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 vinyldns.core.domain
+
+import vinyldns.core.crypto.CryptoAlgebra
+
+final case class Encrypted private (value: String) extends AnyVal
+
+object Encryption {
+ def apply(crypto: CryptoAlgebra, value: String): Encrypted = Encrypted(crypto.encrypt(value))
+ def decrypt(crypto: CryptoAlgebra, x: Encrypted): String = crypto.decrypt(x.value)
+}
+
+object EncryptFromJson {
+ def fromString(name: String): Either[String, Encrypted] =
+ Option(Encrypted(name)).toRight[String](s"Unsupported format")
+}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala b/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala
index c7805e74c..85642dd22 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/SingleChangeError.scala
@@ -31,12 +31,12 @@ object DomainValidationErrorType extends Enumeration {
// NOTE: once defined, an error code type cannot be changed!
val ChangeLimitExceeded, BatchChangeIsEmpty, GroupDoesNotExist, NotAMemberOfOwnerGroup,
InvalidDomainName, InvalidCname, InvalidLength, InvalidEmail, InvalidRecordType, InvalidPortNumber,
- InvalidIpv4Address, InvalidIpv6Address, InvalidIPAddress, InvalidTTL, InvalidMxPreference,
- InvalidBatchRecordType, ZoneDiscoveryError, RecordAlreadyExists, RecordDoesNotExist,
- CnameIsNotUniqueError, UserIsNotAuthorized, UserIsNotAuthorizedError, RecordNameNotUniqueInBatch,
+ InvalidIpv4Address, InvalidIpv6Address, InvalidIPAddress, InvalidTTL, InvalidMX_NAPTR_SRVData, InvalidNaptrFlag,
+ InvalidNaptrRegexp, InvalidBatchRecordType, ZoneDiscoveryError, RecordAlreadyExists, RecordDoesNotExist,
+ InvalidUpdateRequest, CnameIsNotUniqueError, UserIsNotAuthorized, UserIsNotAuthorizedError, RecordNameNotUniqueInBatch,
RecordInReverseZoneError, HighValueDomainError, MissingOwnerGroupId, ExistingMultiRecordError,
NewMultiRecordError, CnameAtZoneApexError, RecordRequiresManualReview, UnsupportedOperation,
- DeleteRecordDataDoesNotExist = Value
+ DeleteRecordDataDoesNotExist, InvalidIPv4CName, InvalidBatchRequest, NotApprovedNSError = Value
// $COVERAGE-OFF$
def from(error: DomainValidationError): DomainValidationErrorType =
@@ -55,11 +55,14 @@ object DomainValidationErrorType extends Enumeration {
case _: InvalidIpv6Address => InvalidIpv6Address
case _: InvalidIPAddress => InvalidIPAddress
case _: InvalidTTL => InvalidTTL
- case _: InvalidMxPreference => InvalidMxPreference
+ case _: InvalidMX_NAPTR_SRVData => InvalidMX_NAPTR_SRVData
+ case _: InvalidNaptrFlag => InvalidNaptrFlag
+ case _: InvalidNaptrRegexp => InvalidNaptrRegexp
case _: InvalidBatchRecordType => InvalidBatchRecordType
case _: ZoneDiscoveryError => ZoneDiscoveryError
case _: RecordAlreadyExists => RecordAlreadyExists
case _: RecordDoesNotExist => RecordDoesNotExist
+ case _: InvalidUpdateRequest => InvalidUpdateRequest
case _: CnameIsNotUniqueError => CnameIsNotUniqueError
case _: UserIsNotAuthorized => UserIsNotAuthorized
case _: UserIsNotAuthorizedError => UserIsNotAuthorizedError
@@ -73,6 +76,9 @@ object DomainValidationErrorType extends Enumeration {
case _: RecordRequiresManualReview => RecordRequiresManualReview
case _: UnsupportedOperation => UnsupportedOperation
case _: DeleteRecordDataDoesNotExist => DeleteRecordDataDoesNotExist
+ case _: InvalidIPv4CName => InvalidIPv4CName
+ case _: InvalidBatchRequest => InvalidBatchRequest
+ case _: NotApprovedNSError => NotApprovedNSError
}
// $COVERAGE-ON$
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/auth/AuthPrincipal.scala b/modules/core/src/main/scala/vinyldns/core/domain/auth/AuthPrincipal.scala
index 03034e964..14e338a17 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/auth/AuthPrincipal.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/auth/AuthPrincipal.scala
@@ -34,7 +34,7 @@ case class AuthPrincipal(signedInUser: User, memberGroupIds: Seq[String]) {
def isTestUser: Boolean = signedInUser.isTest
- val secretKey: String = signedInUser.secretKey
+ val secretKey: String = signedInUser.secretKey.value
val userId: String = signedInUser.id
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChange.scala b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChange.scala
index 71f1da460..43d29c0e2 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChange.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChange.scala
@@ -92,6 +92,12 @@ object BatchChangeStatus extends Enumeration {
case _ => BatchChangeStatus.Complete
}
}
+
+ private val valueMap =
+ BatchChangeStatus.values.map(v => v.toString.toLowerCase -> v).toMap
+
+ def find(status: String): Option[BatchChangeStatus] =
+ valueMap.get(status.toLowerCase)
}
object BatchChangeApprovalStatus extends Enumeration {
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeRepository.scala
index 9901e171c..6751e587e 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeRepository.scala
@@ -18,6 +18,7 @@ package vinyldns.core.domain.batch
import cats.effect.IO
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
import vinyldns.core.repository.Repository
// $COVERAGE-OFF$
@@ -29,8 +30,12 @@ trait BatchChangeRepository extends Repository {
def getBatchChangeSummaries(
userId: Option[String],
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int] = None,
maxItems: Int = 100,
+ batchStatus: Option[BatchChangeStatus] = None,
approvalStatus: Option[BatchChangeApprovalStatus] = None
): IO[BatchChangeSummaryList]
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeSummary.scala b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeSummary.scala
index bd47619c2..6dbbde594 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeSummary.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/batch/BatchChangeSummary.scala
@@ -91,5 +91,9 @@ case class BatchChangeSummaryList(
nextId: Option[Int] = None,
maxItems: Int = 100,
ignoreAccess: Boolean = false,
- approvalStatus: Option[BatchChangeApprovalStatus] = None
+ batchStatus: Option[BatchChangeStatus] = None,
+ approvalStatus: Option[BatchChangeApprovalStatus] = None,
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/batch/SingleChange.scala b/modules/core/src/main/scala/vinyldns/core/domain/batch/SingleChange.scala
index 5483820a4..87cc761ed 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/batch/SingleChange.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/batch/SingleChange.scala
@@ -46,11 +46,11 @@ sealed trait SingleChange {
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
}
- def withDoesNotExistMessage(error: String): SingleChange = this match {
+ def withDoesNotExistMessage: SingleChange = this match {
case add: SingleAddChange =>
- add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
+ add.copy(status = SingleChangeStatus.Failed)
case delete: SingleDeleteRRSetChange =>
- delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error))
+ delete.copy(status = SingleChangeStatus.Complete)
}
def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange =
@@ -69,18 +69,16 @@ sealed trait SingleChange {
)
}
- def complete(message: Option[String], completeRecordChangeId: String, recordSetId: String): SingleChange = this match {
+ def complete(completeRecordChangeId: String, recordSetId: String): SingleChange = this match {
case add: SingleAddChange =>
add.copy(
status = SingleChangeStatus.Complete,
- systemMessage = message,
recordChangeId = Some(completeRecordChangeId),
recordSetId = Some(recordSetId)
)
case delete: SingleDeleteRRSetChange =>
delete.copy(
status = SingleChangeStatus.Complete,
- systemMessage = message,
recordChangeId = Some(completeRecordChangeId),
recordSetId = Some(recordSetId)
)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupChangeRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupChangeRepository.scala
index 408a9345c..c51a5b73b 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupChangeRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/membership/GroupChangeRepository.scala
@@ -28,7 +28,7 @@ trait GroupChangeRepository extends Repository {
def getGroupChanges(
groupId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int
): IO[ListGroupChangesResults]
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/membership/ListGroupChangesResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/membership/ListGroupChangesResults.scala
index 66b71c028..1ceeabf80 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/membership/ListGroupChangesResults.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/membership/ListGroupChangesResults.scala
@@ -18,5 +18,5 @@ package vinyldns.core.domain.membership
final case class ListGroupChangesResults(
changes: Seq[GroupChange],
- lastEvaluatedTimeStamp: Option[String]
+ nextId: Option[Int]
)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/membership/User.scala b/modules/core/src/main/scala/vinyldns/core/domain/membership/User.scala
index 552358a2a..8cfe77e75 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/membership/User.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/membership/User.scala
@@ -21,6 +21,7 @@ import java.util.UUID
import org.apache.commons.lang3.RandomStringUtils
import java.time.Instant
import vinyldns.core.crypto.CryptoAlgebra
+import vinyldns.core.domain.{Encrypted, Encryption}
import vinyldns.core.domain.membership.LockStatus.LockStatus
import java.time.temporal.ChronoUnit
@@ -32,7 +33,7 @@ object LockStatus extends Enumeration {
final case class User(
userName: String,
accessKey: String,
- secretKey: String,
+ secretKey: Encrypted,
firstName: Option[String] = None,
lastName: Option[String] = None,
email: Option[String] = None,
@@ -48,10 +49,10 @@ final case class User(
this.copy(lockStatus = lockStatus)
def regenerateCredentials(): User =
- copy(accessKey = User.generateKey, secretKey = User.generateKey)
+ copy(accessKey = User.generateKey, secretKey = Encrypted(User.generateKey))
def withEncryptedSecretKey(cryptoAlgebra: CryptoAlgebra): User =
- copy(secretKey = cryptoAlgebra.encrypt(secretKey))
+ copy(secretKey = Encryption.apply(cryptoAlgebra, secretKey.value))
override def toString: String = {
val sb = new StringBuilder
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/ChangeSet.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/ChangeSet.scala
index 290e1fbf2..31ee25d88 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/ChangeSet.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/ChangeSet.scala
@@ -50,6 +50,11 @@ case class ChangeSet(
status: ChangeSetStatus
) {
+ def withRecordSetChange(recordSetChanges: Seq[RecordSetChange]): ChangeSet =
+ copy(
+ changes = recordSetChanges
+ )
+
def complete(change: RecordSetChange): ChangeSet = {
val updatedChanges = this.changes.filterNot(_.id == change.id) :+ change
if (isFinished)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetChangesResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetChangesResults.scala
index b5d1141d5..58996d728 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetChangesResults.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetChangesResults.scala
@@ -18,7 +18,14 @@ package vinyldns.core.domain.record
case class ListRecordSetChangesResults(
items: List[RecordSetChange] = List[RecordSetChange](),
- nextId: Option[String] = None,
- startFrom: Option[String] = None,
+ nextId: Option[Int] = None,
+ startFrom: Option[Int] = None,
maxItems: Int = 100
)
+
+case class ListFailedRecordSetChangesResults(
+ items: List[RecordSetChange] = List[RecordSetChange](),
+ nextId: Int = 0,
+ startFrom: Int = 0,
+ maxItems: Int = 100
+ )
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetResults.scala
index f076ab457..1dc47182b 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetResults.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/ListRecordSetResults.scala
@@ -18,6 +18,7 @@ package vinyldns.core.domain.record
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
object NameSort extends Enumeration {
type NameSort = Value
@@ -26,6 +27,19 @@ object NameSort extends Enumeration {
def find(value: String): Value = value.toUpperCase match {
case "DESC" => NameSort.DESC
case _ => NameSort.ASC
+
+ }
+}
+
+object RecordTypeSort extends Enumeration {
+ type RecordTypeSort = Value
+ val ASC, DESC, NONE = Value
+
+ def find(value: String): Value = value.toUpperCase match {
+ case "DESC" => RecordTypeSort.DESC
+ case "ASC" => RecordTypeSort.ASC
+ case _ => RecordTypeSort.NONE
+
}
}
@@ -37,5 +51,6 @@ case class ListRecordSetResults(
recordNameFilter: Option[String] = None,
recordTypeFilter: Option[Set[RecordType]] = None,
recordOwnerGroupFilter: Option[String] = None,
- nameSort: NameSort
+ nameSort: NameSort,
+ recordTypeSort: RecordTypeSort
)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordChangeRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordChangeRepository.scala
index b873dc378..6b5e507ac 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordChangeRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordChangeRepository.scala
@@ -18,6 +18,7 @@ package vinyldns.core.domain.record
import cats.effect._
import scalikejdbc.DB
+import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.repository.Repository
trait RecordChangeRepository extends Repository {
@@ -25,10 +26,18 @@ trait RecordChangeRepository extends Repository {
def save(db: DB, changeSet: ChangeSet): IO[ChangeSet]
def listRecordSetChanges(
- zoneId: String,
- startFrom: Option[String] = None,
- maxItems: Int = 100
+ zoneId: Option[String],
+ startFrom: Option[Int] = None,
+ maxItems: Int = 100,
+ fqdn: Option[String] = None,
+ recordType: Option[RecordType] = None
): IO[ListRecordSetChangesResults]
def getRecordSetChange(zoneId: String, changeId: String): IO[Option[RecordSetChange]]
+
+ def listFailedRecordSetChanges(zoneId: Option[String],
+ maxItems: Int = 100,
+ startFrom: Int = 0
+ ): IO[ListFailedRecordSetChangesResults]
+
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordData.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordData.scala
index dcb36ed6a..6c76fecf4 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordData.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordData.scala
@@ -175,21 +175,17 @@ object SRVData {
def fromString(value: String): Option[SRVData] =
Option(value).flatMap { v =>
val parts = v.split(' ')
- if (parts.length != 7) {
- None
- } else {
- for {
- priority <- toInt(parts(0))
- weight <- toInt(parts(1))
- port <- toInt(parts(2))
- target = Fqdn(parts(3))
- } yield SRVData(
- priority,
- weight,
- port,
- target
- )
- }
+ for {
+ priority <- toInt(parts(0))
+ weight <- toInt(parts(1))
+ port <- toInt(parts(2))
+ target = Fqdn(parts(3))
+ } yield SRVData(
+ priority,
+ weight,
+ port,
+ target
+ )
}
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala
index da227dbca..6cdc2482c 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSet.scala
@@ -16,6 +16,8 @@
package vinyldns.core.domain.record
+import vinyldns.core.domain.record.OwnerShipTransferStatus.OwnerShipTransferStatus
+
import java.util.UUID
import java.time.Instant
@@ -33,6 +35,11 @@ object RecordSetStatus extends Enumeration {
val Active, Inactive, Pending, PendingUpdate, PendingDelete = Value
}
+object OwnerShipTransferStatus extends Enumeration {
+ type OwnerShipTransferStatus = Value
+ val AutoApproved, Cancelled, ManuallyApproved, ManuallyRejected, Requested, PendingReview, None = Value
+}
+
import RecordSetStatus._
import RecordType._
@@ -48,6 +55,7 @@ case class RecordSet(
id: String = UUID.randomUUID().toString,
account: String = "system",
ownerGroupId: Option[String] = None,
+ recordSetGroupChange: Option[OwnerShipTransfer] = None,
fqdn: Option[String] = None
) {
@@ -69,10 +77,25 @@ case class RecordSet(
sb.append("account=\"").append(account).append("\"; ")
sb.append("status=\"").append(status.toString).append("\"; ")
sb.append("records=\"").append(records.toString).append("\"; ")
- sb.append("ownerGroupId=\"").append(ownerGroupId).append("\"")
+ sb.append("ownerGroupId=\"").append(ownerGroupId).append("\"; ")
+ sb.append("recordSetGroupChange=\"").append(recordSetGroupChange.toString).append("\"; ")
sb.append("fqdn=\"").append(fqdn).append("\"")
sb.append("]")
sb.toString
}
}
+case class OwnerShipTransfer(
+ ownerShipTransferStatus: OwnerShipTransferStatus,
+ requestedOwnerGroupId: Option[String] = None
+ ){
+
+ override def toString: String = {
+ val sb = new StringBuilder
+ sb.append("OwnerShipTransfer: [")
+ sb.append("ownerShipTransferStatus=\"").append(ownerShipTransferStatus.toString).append("\"; ")
+ sb.append("requestedOwnerGroupId=\"").append(requestedOwnerGroupId.toString).append("\"")
+ sb.append("]")
+ sb.toString
+ }}
+
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSetRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSetRepository.scala
index bafa7d8ec..1699855f0 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSetRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/record/RecordSetRepository.scala
@@ -20,6 +20,7 @@ import cats.effect._
import scalikejdbc.DB
import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.repository.Repository
trait RecordSetRepository extends Repository {
@@ -33,7 +34,8 @@ trait RecordSetRepository extends Repository {
recordNameFilter: Option[String],
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
- nameSort: NameSort
+ nameSort: NameSort,
+ recordTypeSort: RecordTypeSort
): IO[ListRecordSetResults]
def getRecordSets(zoneId: String, name: String, typ: RecordType): IO[List[RecordSet]]
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala
index 5011a79d6..002f7280f 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZoneChangesResults.scala
@@ -22,3 +22,19 @@ case class ListZoneChangesResults(
startFrom: Option[String] = None,
maxItems: Int = 100
)
+
+case class ListDeletedZonesChangeResults(
+ zoneDeleted: List[ZoneChange] = List[ZoneChange](),
+ nextId: Option[String] = None,
+ startFrom: Option[String] = None,
+ maxItems: Int = 100,
+ ignoreAccess: Boolean = false,
+ zoneChangeFilter: Option[String] = None
+)
+
+case class ListFailedZoneChangesResults(
+ items: List[ZoneChange] = List[ZoneChange](),
+ nextId: Int = 0,
+ startFrom: Int = 0,
+ maxItems: Int = 100
+ )
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZonesResults.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZonesResults.scala
index 8d03b0073..de7dd1b17 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZonesResults.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ListZonesResults.scala
@@ -22,5 +22,6 @@ case class ListZonesResults(
startFrom: Option[String] = None,
maxItems: Int = 100,
ignoreAccess: Boolean = false,
- zonesFilter: Option[String] = None
+ zonesFilter: Option[String] = None,
+ includeReverse: Boolean = true
)
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/Zone.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/Zone.scala
index 4ed0f2a60..1d03fb21e 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/Zone.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/Zone.scala
@@ -25,6 +25,7 @@ import pureconfig.{ConfigReader, ConfigSource}
import pureconfig.error.CannotConvert
import pureconfig.generic.auto._
import vinyldns.core.crypto.CryptoAlgebra
+import vinyldns.core.domain.{Encrypted, Encryption}
import scala.collection.JavaConverters._
object ZoneStatus extends Enumeration {
@@ -47,6 +48,8 @@ final case class Zone(
shared: Boolean = false,
acl: ZoneACL = ZoneACL(),
adminGroupId: String = "system",
+ recurrenceSchedule: Option[String] = None,
+ scheduleRequestor: Option[String] = None,
latestSync: Option[Instant] = None,
isTest: Boolean = false,
backendId: Option[String] = None
@@ -75,6 +78,8 @@ final case class Zone(
sb.append("reverse=\"").append(isReverse).append("\"; ")
sb.append("isTest=\"").append(isTest).append("\"; ")
sb.append("created=\"").append(created).append("\"; ")
+ recurrenceSchedule.map(sb.append("recurrenceSchedule=\"").append(_).append("\"; "))
+ scheduleRequestor.map(sb.append("scheduleRequestor=\"").append(_).append("\"; "))
updated.map(sb.append("updated=\"").append(_).append("\"; "))
latestSync.map(sb.append("latestSync=\"").append(_).append("\"; "))
sb.append("]")
@@ -95,7 +100,9 @@ object Zone {
acl = acl,
adminGroupId = adminGroupId,
backendId = backendId,
- isTest = isTest
+ isTest = isTest,
+ recurrenceSchedule = recurrenceSchedule,
+ scheduleRequestor = scheduleRequestor
)
}
@@ -110,7 +117,9 @@ object Zone {
shared = shared,
acl = acl,
adminGroupId = adminGroupId,
- backendId = backendId
+ backendId = backendId,
+ recurrenceSchedule = recurrenceSchedule,
+ scheduleRequestor = scheduleRequestor
)
}
}
@@ -123,7 +132,9 @@ final case class CreateZoneInput(
shared: Boolean = false,
acl: ZoneACL = ZoneACL(),
adminGroupId: String,
- backendId: Option[String] = None
+ backendId: Option[String] = None,
+ recurrenceSchedule: Option[String] = None,
+ scheduleRequestor: Option[String] = None
)
final case class UpdateZoneInput(
@@ -135,6 +146,8 @@ final case class UpdateZoneInput(
shared: Boolean = false,
acl: ZoneACL = ZoneACL(),
adminGroupId: String,
+ recurrenceSchedule: Option[String] = None,
+ scheduleRequestor: Option[String] = None,
backendId: Option[String] = None
)
@@ -178,16 +191,16 @@ object Algorithm {
case class ZoneConnection(
name: String,
keyName: String,
- key: String,
+ key: Encrypted,
primaryServer: String,
algorithm: Algorithm = Algorithm.HMAC_MD5
) {
def encrypted(crypto: CryptoAlgebra): ZoneConnection =
- copy(key = crypto.encrypt(key))
+ copy(key = Encryption.apply(crypto, key.value))
def decrypted(crypto: CryptoAlgebra): ZoneConnection =
- copy(key = crypto.decrypt(key))
+ copy(key = Encrypted(Encryption.decrypt(crypto, key)))
override def toString: String = {
val sb = new StringBuilder
@@ -230,7 +243,7 @@ object ConfiguredDnsConnections {
if (connectionConfig.hasPath("algorithm"))
Algorithm.Map.getOrElse(connectionConfig.getString("algorithm"), Algorithm.HMAC_MD5)
else Algorithm.HMAC_MD5
- ZoneConnection(name, keyName, key, primaryServer, algorithm).encrypted(crypto)
+ ZoneConnection(name, keyName, Encrypted(key), primaryServer, algorithm).encrypted(crypto)
}
val defaultTransferConnection = {
@@ -243,7 +256,7 @@ object ConfiguredDnsConnections {
if (connectionConfig.hasPath("algorithm"))
Algorithm.Map.getOrElse(connectionConfig.getString("algorithm"), Algorithm.HMAC_MD5)
else Algorithm.HMAC_MD5
- ZoneConnection(name, keyName, key, primaryServer, algorithm).encrypted(crypto)
+ ZoneConnection(name, keyName, Encrypted(key), primaryServer, algorithm).encrypted(crypto)
}
val dnsBackends = {
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChange.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChange.scala
index 287eb2cf6..3474d403d 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChange.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChange.scala
@@ -28,7 +28,7 @@ object ZoneChangeStatus extends Enumeration {
object ZoneChangeType extends Enumeration {
type ZoneChangeType = Value
- val Create, Update, Delete, Sync = Value
+ val Create, Update, Delete, Sync, AutomatedSync = Value
}
import vinyldns.core.domain.zone.ZoneChangeStatus._
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala
index 57a44e217..15ffcbebd 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneChangeRepository.scala
@@ -17,6 +17,7 @@
package vinyldns.core.domain.zone
import cats.effect._
+import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.core.repository.Repository
trait ZoneChangeRepository extends Repository {
@@ -28,4 +29,17 @@ trait ZoneChangeRepository extends Repository {
startFrom: Option[String] = None,
maxItems: Int = 100
): IO[ListZoneChangesResults]
+
+ def listDeletedZones(
+ authPrincipal: AuthPrincipal,
+ zoneNameFilter: Option[String] = None,
+ startFrom: Option[String] = None,
+ maxItems: Int = 100,
+ ignoreAccess: Boolean = false
+ ): IO[ListDeletedZonesChangeResults]
+
+ def listFailedZoneChanges(
+ maxItems: Int = 100,
+ startFrom: Int= 0
+ ): IO[ListFailedZoneChangesResults]
}
diff --git a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala
index ec8e82af9..bb0d3e7e2 100644
--- a/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala
+++ b/modules/core/src/main/scala/vinyldns/core/domain/zone/ZoneRepository.scala
@@ -29,6 +29,8 @@ trait ZoneRepository extends Repository {
def getZones(zoneId: Set[String]): IO[Set[Zone]]
+ def getAllZonesWithSyncSchedule: IO[Set[Zone]]
+
def getZoneByName(zoneName: String): IO[Option[Zone]]
def getZonesByNames(zoneNames: Set[String]): IO[Set[Zone]]
@@ -40,7 +42,8 @@ trait ZoneRepository extends Repository {
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults]
def listZones(
@@ -48,7 +51,8 @@ trait ZoneRepository extends Repository {
zoneNameFilter: Option[String] = None,
startFrom: Option[String] = None,
maxItems: Int = 100,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults]
def getZonesByAdminGroupId(adminGroupId: String): IO[List[Zone]]
diff --git a/modules/core/src/main/scala/vinyldns/core/health/HealthCheck.scala b/modules/core/src/main/scala/vinyldns/core/health/HealthCheck.scala
index 0432a8581..00b872ced 100644
--- a/modules/core/src/main/scala/vinyldns/core/health/HealthCheck.scala
+++ b/modules/core/src/main/scala/vinyldns/core/health/HealthCheck.scala
@@ -18,6 +18,7 @@ package vinyldns.core.health
import cats.effect.IO
import org.slf4j.LoggerFactory
+import java.io.{PrintWriter, StringWriter}
object HealthCheck {
@@ -31,7 +32,9 @@ object HealthCheck {
def asHealthCheck(caller: Class[_]): HealthCheck =
io.map {
case Left(err) =>
- logger.error(s"HealthCheck for ${caller.getCanonicalName} Failed", err)
+ val errorMessage = new StringWriter
+ err.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"HealthCheck for ${caller.getCanonicalName} failed. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
val msg = Option(err.getMessage).getOrElse("no message from error")
Left(
HealthCheckError(s"${caller.getCanonicalName} health check failed with msg='${msg}'")
diff --git a/modules/core/src/main/scala/vinyldns/core/notifier/AllNotifiers.scala b/modules/core/src/main/scala/vinyldns/core/notifier/AllNotifiers.scala
index 7a13c3134..d721f40e0 100644
--- a/modules/core/src/main/scala/vinyldns/core/notifier/AllNotifiers.scala
+++ b/modules/core/src/main/scala/vinyldns/core/notifier/AllNotifiers.scala
@@ -20,6 +20,7 @@ import cats.effect.{ContextShift, IO}
import cats.implicits._
import org.slf4j.LoggerFactory
import vinyldns.core.route.Monitored
+import java.io.{PrintWriter, StringWriter}
final case class AllNotifiers(notifiers: List[Notifier])(implicit val cs: ContextShift[IO])
extends Monitored {
@@ -34,8 +35,10 @@ final case class AllNotifiers(notifiers: List[Notifier])(implicit val cs: Contex
def notify(notifier: Notifier, notification: Notification[_]): IO[Unit] =
monitor(notifier.getClass.getSimpleName) {
notifier.notify(notification).handleErrorWith { e =>
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
IO {
- logger.error(s"Notifier ${notifier.getClass.getSimpleName} failed.", e)
+ logger.error(s"Notifier ${notifier.getClass.getSimpleName} failed. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
}
}
}
diff --git a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala
index 78a65cc4a..49f69a321 100644
--- a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala
+++ b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierLoader.scala
@@ -15,21 +15,21 @@
*/
package vinyldns.core.notifier
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO
import cats.implicits._
import cats.effect.ContextShift
object NotifierLoader {
- def loadAll(configs: List[NotifierConfig], userRepository: UserRepository)(
- implicit cs: ContextShift[IO]
+ def loadAll(configs: List[NotifierConfig], userRepository: UserRepository, groupRepository: GroupRepository)(
+ implicit cs: ContextShift[IO]
): IO[AllNotifiers] =
for {
- notifiers <- configs.parTraverse(load(_, userRepository))
+ notifiers <- configs.parTraverse(load(_, userRepository, groupRepository))
} yield AllNotifiers(notifiers)
- def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier] =
+ def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier] =
for {
provider <- IO(
Class
@@ -38,7 +38,7 @@ object NotifierLoader {
.newInstance()
.asInstanceOf[NotifierProvider]
)
- notifier <- provider.load(config, userRepository)
+ notifier <- provider.load(config, userRepository, groupRepository)
} yield notifier
}
diff --git a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala
index c5fe76a98..71d2ba0e0 100644
--- a/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala
+++ b/modules/core/src/main/scala/vinyldns/core/notifier/NotifierProvider.scala
@@ -15,9 +15,9 @@
*/
package vinyldns.core.notifier
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO
trait NotifierProvider {
- def load(config: NotifierConfig, userRepository: UserRepository): IO[Notifier]
+ def load(config: NotifierConfig, userRepository: UserRepository, groupRepository: GroupRepository): IO[Notifier]
}
diff --git a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala
index f61bb3d87..2e0a852c9 100644
--- a/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala
+++ b/modules/core/src/main/scala/vinyldns/core/protobuf/ProtobufConversions.scala
@@ -26,7 +26,7 @@ import vinyldns.core.domain.membership.{LockStatus, User, UserChange, UserChange
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record._
import vinyldns.core.domain.zone._
-import vinyldns.core.domain.{Fqdn, record, zone}
+import vinyldns.core.domain.{Encrypted, Fqdn, record, zone}
import vinyldns.proto.VinylDNSProto
import scala.collection.JavaConverters._
@@ -100,9 +100,23 @@ trait ProtobufConversions {
records =
rs.getRecordList.asScala.map(rd => fromPB(rd, RecordType.withName(rs.getTyp))).toList,
account = rs.getAccount,
- ownerGroupId = if (rs.hasOwnerGroupId) Some(rs.getOwnerGroupId) else None
+ ownerGroupId = if (rs.hasOwnerGroupId) Some(rs.getOwnerGroupId) else None ,
+ recordSetGroupChange = if (rs.hasRecordSetGroupChange) Some(fromPB(rs.getRecordSetGroupChange)) else None,
)
+ def fromPB(rsa: VinylDNSProto.ownerShipTransfer): OwnerShipTransfer =
+ record.OwnerShipTransfer(
+ ownerShipTransferStatus = OwnerShipTransferStatus.withName(rsa.getOwnerShipTransferStatus),
+ requestedOwnerGroupId = if (rsa.hasRequestedOwnerGroupId) Some(rsa.getRequestedOwnerGroupId) else None)
+
+ def toPB(rsa: OwnerShipTransfer): VinylDNSProto.ownerShipTransfer = {
+ val builder = VinylDNSProto.ownerShipTransfer
+ .newBuilder()
+ .setOwnerShipTransferStatus(rsa.ownerShipTransferStatus.toString)
+ rsa.requestedOwnerGroupId.foreach(id => builder.setRequestedOwnerGroupId(id))
+ builder.build()
+ }
+
def fromPB(zn: VinylDNSProto.Zone): Zone = {
val pbStatus = zn.getStatus
val status =
@@ -125,7 +139,9 @@ trait ProtobufConversions {
adminGroupId = zn.getAdminGroupId,
latestSync = if (zn.hasLatestSync) Some(Instant.ofEpochMilli(zn.getLatestSync)) else None,
isTest = zn.getIsTest,
- backendId = if (zn.hasBackendId) Some(zn.getBackendId) else None
+ backendId = if (zn.hasBackendId) Some(zn.getBackendId) else None,
+ recurrenceSchedule = if (zn.hasRecurrenceSchedule) Some(zn.getRecurrenceSchedule) else None,
+ scheduleRequestor = if (zn.hasScheduleRequestor) Some(zn.getScheduleRequestor) else None
)
}
@@ -133,7 +149,7 @@ trait ProtobufConversions {
ZoneConnection(
zc.getName,
zc.getKeyName,
- zc.getKey,
+ Encrypted(zc.getKey),
zc.getPrimaryServer,
fromPB(zc.getAlgorithm)
)
@@ -375,6 +391,7 @@ trait ProtobufConversions {
rs.updated.foreach(dt => builder.setUpdated(dt.toEpochMilli))
rs.ownerGroupId.foreach(id => builder.setOwnerGroupId(id))
+ rs.recordSetGroupChange.foreach(rsg => builder.setRecordSetGroupChange(toPB(rsg)))
// Map the records, first map to bytes, and then map the bytes to a record data instance
rs.records.map(toRecordData).foreach(rd => builder.addRecord(rd))
@@ -401,6 +418,8 @@ trait ProtobufConversions {
zone.transferConnection.foreach(cn => builder.setTransferConnection(toPB(cn)))
zone.latestSync.foreach(dt => builder.setLatestSync(dt.toEpochMilli))
zone.backendId.foreach(bid => builder.setBackendId(bid))
+ zone.recurrenceSchedule.foreach(rs => builder.setRecurrenceSchedule(rs))
+ zone.scheduleRequestor.foreach(rs => builder.setScheduleRequestor(rs))
builder.build()
}
@@ -418,7 +437,7 @@ trait ProtobufConversions {
.newBuilder()
.setName(conn.name)
.setKeyName(conn.keyName)
- .setKey(conn.key)
+ .setKey(conn.key.value)
.setPrimaryServer(conn.primaryServer)
.setAlgorithm(toPB(conn.algorithm))
.build()
@@ -442,7 +461,7 @@ trait ProtobufConversions {
User(
data.getUserName,
data.getAccessKey,
- data.getSecretKey,
+ Encrypted(data.getSecretKey),
if (data.hasFirstName) Some(data.getFirstName) else None,
if (data.hasLastName) Some(data.getLastName) else None,
if (data.hasEmail) Some(data.getEmail) else None,
@@ -459,7 +478,7 @@ trait ProtobufConversions {
.newBuilder()
.setUserName(user.userName)
.setAccessKey(user.accessKey)
- .setSecretKey(user.secretKey)
+ .setSecretKey(user.secretKey.value)
.setCreated(user.created.toEpochMilli)
.setId(user.id)
.setIsSuper(user.isSuper)
diff --git a/modules/core/src/main/scala/vinyldns/core/route/Monitor.scala b/modules/core/src/main/scala/vinyldns/core/route/Monitor.scala
index d0aa569d5..34edda917 100644
--- a/modules/core/src/main/scala/vinyldns/core/route/Monitor.scala
+++ b/modules/core/src/main/scala/vinyldns/core/route/Monitor.scala
@@ -20,7 +20,7 @@ import cats.effect._
import nl.grons.metrics.scala.{Histogram, Meter, MetricName}
import org.slf4j.{Logger, LoggerFactory}
import vinyldns.core.Instrumented
-
+import java.io.{PrintWriter, StringWriter}
import scala.collection._
trait Monitored {
@@ -52,7 +52,9 @@ trait Monitored {
IO(t)
case Left(e) =>
- logger.error(s"Finished $id; success=false; duration=$duration seconds", e)
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Finished $id; success=false; duration=$duration seconds. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
IO.raiseError(e)
}
}
diff --git a/modules/core/src/main/scala/vinyldns/core/task/TaskScheduler.scala b/modules/core/src/main/scala/vinyldns/core/task/TaskScheduler.scala
index 32f8c5cad..701fd1fa9 100644
--- a/modules/core/src/main/scala/vinyldns/core/task/TaskScheduler.scala
+++ b/modules/core/src/main/scala/vinyldns/core/task/TaskScheduler.scala
@@ -20,7 +20,7 @@ import cats.implicits._
import fs2._
import org.slf4j.LoggerFactory
import vinyldns.core.route.Monitored
-
+import java.io.{PrintWriter, StringWriter}
import scala.concurrent.duration.FiniteDuration
// Interface for all Tasks that need to be run
@@ -91,7 +91,9 @@ object TaskScheduler extends Monitored {
def runOnceSafely(task: Task): IO[Unit] =
monitor(s"task.${task.name}") {
claimTask().bracket(runTask)(releaseTask).handleError { error =>
- logger.error(s"""Unexpected error running task; taskName="${task.name}" """, error)
+ val errorMessage = new StringWriter
+ error.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"""Unexpected error running task; taskName="${task.name}". Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")} """)
}
}
diff --git a/modules/core/src/test/scala/vinyldns/core/TestMembershipData.scala b/modules/core/src/test/scala/vinyldns/core/TestMembershipData.scala
index 4789b7f3a..223a3dac6 100644
--- a/modules/core/src/test/scala/vinyldns/core/TestMembershipData.scala
+++ b/modules/core/src/test/scala/vinyldns/core/TestMembershipData.scala
@@ -16,6 +16,8 @@
package vinyldns.core
+
+import vinyldns.core.domain.Encrypted
import java.time.Instant
import java.time.temporal.ChronoUnit
import vinyldns.core.domain.auth.AuthPrincipal
@@ -29,18 +31,18 @@ object TestMembershipData {
id = "ok",
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "okAccessKey",
- secretKey = "okSecretKey",
+ secretKey = Encrypted("okSecretKey"),
firstName = Some("ok"),
lastName = Some("ok"),
email = Some("test@test.com")
)
- val dummyUser = User("dummyName", "dummyAccess", "dummySecret")
- val superUser = User("super", "superAccess", "superSecret", isSuper = true)
- val xyzUser = User("xyz", "xyzAccess", "xyzSecret")
- val supportUser = User("support", "supportAccess", "supportSecret", isSupport = true)
- val lockedUser = User("locked", "lockedAccess", "lockedSecret", lockStatus = LockStatus.Locked)
- val sharedZoneUser = User("sharedZoneAdmin", "sharedAccess", "sharedSecret")
+ val dummyUser = User("dummyName", "dummyAccess", Encrypted("dummySecret"))
+ val superUser = User("super", "superAccess", Encrypted("superSecret"), isSuper = true)
+ val xyzUser = User("xyz", "xyzAccess", Encrypted("xyzSecret"))
+ val supportUser = User("support", "supportAccess", Encrypted("supportSecret"), isSupport = true)
+ val lockedUser = User("locked", "lockedAccess", Encrypted("lockedSecret"), lockStatus = LockStatus.Locked)
+ val sharedZoneUser = User("sharedZoneAdmin", "sharedAccess", Encrypted("sharedSecret"))
val listOfDummyUsers: List[User] = List.range(0, 200).map { runner =>
User(
@@ -48,7 +50,7 @@ object TestMembershipData {
id = "dummy%03d".format(runner),
created = Instant.now.truncatedTo(ChronoUnit.SECONDS),
accessKey = "dummy",
- secretKey = "dummy"
+ secretKey = Encrypted("dummy")
)
}
@@ -117,15 +119,15 @@ object TestMembershipData {
memberGroupIds = List(abcGroup.id, okGroup.id)
)
- val dummyAuth: AuthPrincipal = AuthPrincipal(dummyUser, Seq(dummyGroup.id))
+ val dummyAuth: AuthPrincipal = AuthPrincipal(dummyUser, Seq(dummyGroup.id, oneUserDummyGroup.id))
- val notAuth: AuthPrincipal = AuthPrincipal(User("not", "auth", "secret"), Seq.empty)
+ val notAuth: AuthPrincipal = AuthPrincipal(User("not", "auth", Encrypted("secret")), Seq.empty)
val sharedAuth: AuthPrincipal = AuthPrincipal(sharedZoneUser, Seq(abcGroup.id))
val supportUserAuth: AuthPrincipal = AuthPrincipal(supportUser, Seq(okGroup.id))
- val superUserAuth = AuthPrincipal(superUser, Seq.empty)
+ val superUserAuth: AuthPrincipal = AuthPrincipal(superUser, Seq.empty)
/* GROUP CHANGES */
val okGroupChange: GroupChange = GroupChange(
diff --git a/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala b/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala
index 1c74f0b86..2b2d4f2b0 100644
--- a/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala
+++ b/modules/core/src/test/scala/vinyldns/core/TestRecordSetData.scala
@@ -37,7 +37,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(AData("10.1.1.1"))
+ List(AData("10.1.1.1")),
+ recordSetGroupChange = None
)
val abcRecord: RecordSet = RecordSet(
@@ -48,7 +49,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(AAAAData("1:2:3:4:5:6:7:8"))
+ List(AAAAData("1:2:3:4:5:6:7:8")),
+ recordSetGroupChange= None
)
val aaaa: RecordSet = RecordSet(
@@ -59,7 +61,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(AAAAData("1:2:3:4:5:6:7:8"))
+ List(AAAAData("1:2:3:4:5:6:7:8")),
+ recordSetGroupChange= None
)
val aaaaOrigin: RecordSet = RecordSet(
@@ -70,7 +73,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(AAAAData("1:2:3:4:5:6:7:8"))
+ List(AAAAData("1:2:3:4:5:6:7:8")),
+ recordSetGroupChange= None
)
val cname: RecordSet = RecordSet(
@@ -81,7 +85,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(CNAMEData(Fqdn("cname")))
+ List(CNAMEData(Fqdn("cname"))),
+ recordSetGroupChange= None
)
val ptrIp4: RecordSet = RecordSet(
@@ -92,7 +97,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(PTRData(Fqdn("ptr")))
+ List(PTRData(Fqdn("ptr"))),
+ recordSetGroupChange= None
)
val ptrIp6: RecordSet = RecordSet(
@@ -103,7 +109,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(PTRData(Fqdn("ptr")))
+ List(PTRData(Fqdn("ptr"))),
+ recordSetGroupChange= None
)
val srv: RecordSet = RecordSet(
@@ -114,7 +121,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(SRVData(1, 2, 3, Fqdn("target")))
+ List(SRVData(1, 2, 3, Fqdn("target"))),
+ recordSetGroupChange= None
)
val naptr: RecordSet = RecordSet(
@@ -125,7 +133,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(NAPTRData(1, 2, "S", "E2U+sip", "", Fqdn("target")))
+ List(NAPTRData(1, 2, "S", "E2U+sip", "", Fqdn("target"))),
+ recordSetGroupChange= None
)
val mx: RecordSet = RecordSet(
@@ -136,7 +145,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(MXData(3, Fqdn("mx")))
+ List(MXData(3, Fqdn("mx"))),
+ recordSetGroupChange= None
)
val ns: RecordSet = RecordSet(
@@ -147,7 +157,8 @@ object TestRecordSetData {
RecordSetStatus.Active,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- records = List(NSData(Fqdn("ns1.test.com")), NSData(Fqdn("ns2.test.com")))
+ records = List(NSData(Fqdn("ns1.test.com")), NSData(Fqdn("ns2.test.com"))),
+ recordSetGroupChange= None
)
val txt: RecordSet = RecordSet(
@@ -158,7 +169,8 @@ object TestRecordSetData {
RecordSetStatus.Pending,
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
- List(TXTData("txt"))
+ List(TXTData("txt")),
+ recordSetGroupChange= None
)
// example at https://tools.ietf.org/html/rfc4034#page-18
@@ -198,7 +210,12 @@ object TestRecordSetData {
Instant.now.truncatedTo(ChronoUnit.MILLIS),
None,
List(AAAAData("1:2:3:4:5:6:7:8")),
- ownerGroupId = Some(okGroup.id)
+ ownerGroupId = Some(okGroup.id),
+ recordSetGroupChange= None
+ )
+
+ val ownerShipTransfer: OwnerShipTransfer = OwnerShipTransfer(
+ OwnerShipTransferStatus.None
)
val sharedZoneRecordNoOwnerGroup: RecordSet =
diff --git a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala
index 8392403ae..c489938f5 100644
--- a/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala
+++ b/modules/core/src/test/scala/vinyldns/core/TestZoneData.scala
@@ -18,6 +18,7 @@ package vinyldns.core
import vinyldns.core.domain.zone._
import TestMembershipData._
+import vinyldns.core.domain.Encrypted
import java.time.Instant
import java.time.temporal.ChronoUnit
@@ -25,7 +26,7 @@ object TestZoneData {
/* ZONE CONNECTIONS */
val testConnection: Option[ZoneConnection] = Some(
- ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")
+ ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")
)
/* ZONES */
@@ -51,9 +52,21 @@ object TestZoneData {
connection = testConnection
)
+ val abcZoneDeleted: Zone = Zone("abc.zone.recordsets.", "test@test.com", adminGroupId = abcGroup.id, status = ZoneStatus.Deleted)
+ val xyzZoneDeleted: Zone = Zone("xyz.zone.recordsets.", "abc@xyz.com", adminGroupId = xyzGroup.id, status = ZoneStatus.Deleted)
+
val zoneDeleted: Zone = Zone(
"some.deleted.zone.",
"test@test.com",
+ adminGroupId = abcGroup.id,
+ status = ZoneStatus.Deleted,
+ connection = testConnection
+ )
+
+ val zoneDeletedOkGroup: Zone = Zone(
+ "some.deleted.zone.",
+ "test@test.com",
+ adminGroupId = okGroup.id,
status = ZoneStatus.Deleted,
connection = testConnection
)
@@ -88,6 +101,22 @@ object TestZoneData {
val zoneUpdate: ZoneChange = zoneChangePending.copy(status = ZoneChangeStatus.Synced)
+ val abcDeletedZoneChange: ZoneChange = ZoneChange(
+ abcZoneDeleted,
+ "ok",
+ ZoneChangeType.Create,
+ ZoneChangeStatus.Synced,
+ created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
+ )
+
+ val xyzDeletedZoneChange: ZoneChange = ZoneChange(
+ xyzZoneDeleted,
+ "ok",
+ ZoneChangeType.Create,
+ ZoneChangeStatus.Synced,
+ created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
+ )
+
def makeTestPendingZoneChange(zone: Zone): ZoneChange =
ZoneChange(zone, "userId", ZoneChangeType.Update, ZoneChangeStatus.Pending)
diff --git a/modules/core/src/test/scala/vinyldns/core/domain/membership/UserChangeSpec.scala b/modules/core/src/test/scala/vinyldns/core/domain/membership/UserChangeSpec.scala
index c2c41da7a..5927f6c52 100644
--- a/modules/core/src/test/scala/vinyldns/core/domain/membership/UserChangeSpec.scala
+++ b/modules/core/src/test/scala/vinyldns/core/domain/membership/UserChangeSpec.scala
@@ -20,11 +20,12 @@ import java.time.Instant
import org.scalatest.EitherValues
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
+import vinyldns.core.domain.Encrypted
import java.time.temporal.ChronoUnit
class UserChangeSpec extends AnyWordSpec with Matchers with EitherMatchers with EitherValues {
- private val newUser = User("foo", "key", "secret")
+ private val newUser = User("foo", "key", Encrypted("secret"))
private val currentDate = Instant.now.truncatedTo(ChronoUnit.MILLIS)
"apply" should {
diff --git a/modules/core/src/test/scala/vinyldns/core/domain/zone/ZoneConnectionSpec.scala b/modules/core/src/test/scala/vinyldns/core/domain/zone/ZoneConnectionSpec.scala
index d536dfd0d..33b1efbd5 100644
--- a/modules/core/src/test/scala/vinyldns/core/domain/zone/ZoneConnectionSpec.scala
+++ b/modules/core/src/test/scala/vinyldns/core/domain/zone/ZoneConnectionSpec.scala
@@ -20,6 +20,7 @@ import cats.scalatest.EitherMatchers
import vinyldns.core.crypto.CryptoAlgebra
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
+import vinyldns.core.domain.Encrypted
class ZoneConnectionSpec extends AnyWordSpec with Matchers with EitherMatchers {
@@ -31,16 +32,16 @@ class ZoneConnectionSpec extends AnyWordSpec with Matchers with EitherMatchers {
"ZoneConnection" should {
"encrypt clear connections" in {
- val test = ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")
+ val test = ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")
- test.encrypted(testCrypto).key shouldBe "encrypted!"
+ test.encrypted(testCrypto).key.value shouldBe "encrypted!"
}
"decrypt connections" in {
- val test = ZoneConnection("vinyldns.", "vinyldns.", "nzisn+4G2ldMn0q1CV3vsg==", "10.1.1.1")
+ val test = ZoneConnection("vinyldns.", "vinyldns.", Encrypted("nzisn+4G2ldMn0q1CV3vsg=="), "10.1.1.1")
val decrypted = test.decrypted(testCrypto)
- decrypted.key shouldBe "decrypted!"
+ decrypted.key.value shouldBe "decrypted!"
}
}
}
diff --git a/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala b/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala
index 849d46948..daa151044 100644
--- a/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala
+++ b/modules/core/src/test/scala/vinyldns/core/notifier/NotifierLoaderSpec.scala
@@ -19,7 +19,7 @@ package vinyldns.core.notifier
import cats.scalatest.{EitherMatchers, EitherValues, ValidatedMatchers}
import com.typesafe.config.{Config, ConfigFactory}
import org.scalatestplus.mockito.MockitoSugar
-import vinyldns.core.domain.membership.UserRepository
+import vinyldns.core.domain.membership.{GroupRepository, UserRepository}
import cats.effect.IO
import org.mockito.Mockito._
@@ -37,13 +37,13 @@ object MockNotifierProvider extends MockitoSugar {
class MockNotifierProvider extends NotifierProvider {
- def load(config: NotifierConfig, userRepo: UserRepository): IO[Notifier] =
+ def load(config: NotifierConfig, userRepo: UserRepository, groupRepo: GroupRepository): IO[Notifier] =
IO.pure(MockNotifierProvider.mockNotifier)
}
class FailingProvider extends NotifierProvider {
- def load(config: NotifierConfig, userRepo: UserRepository): IO[Notifier] =
+ def load(config: NotifierConfig, userRepo: UserRepository, groupRepo: GroupRepository): IO[Notifier] =
IO.raiseError(new IllegalStateException("always failing"))
}
@@ -63,6 +63,7 @@ class NotifierLoaderSpec
val goodConfig = NotifierConfig("vinyldns.core.notifier.MockNotifierProvider", placeholderConfig)
val mockUserRepository: UserRepository = mock[UserRepository]
+ val mockGroupRepository: GroupRepository = mock[GroupRepository]
import MockNotifierProvider._
@@ -73,14 +74,13 @@ class NotifierLoaderSpec
"return some notifier with no configs" in {
- val notifier = NotifierLoader.loadAll(List.empty, mockUserRepository).unsafeRunSync()
-
+ val notifier = NotifierLoader.loadAll(List.empty, mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldBe a[AllNotifiers]
notifier.notify(Notification(3)).unsafeRunSync() shouldBe (())
}
"return a notifier for valid config of one notifier" in {
- val notifier = NotifierLoader.loadAll(List(goodConfig), mockUserRepository).unsafeRunSync()
+ val notifier = NotifierLoader.loadAll(List(goodConfig), mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldNot be(null)
@@ -95,7 +95,7 @@ class NotifierLoaderSpec
"return a notifier for valid config of multiple notifiers" in {
val notifier =
- NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository).unsafeRunSync()
+ NotifierLoader.loadAll(List(goodConfig, goodConfig), mockUserRepository, mockGroupRepository).unsafeRunSync()
notifier shouldNot be(null)
@@ -113,7 +113,7 @@ class NotifierLoaderSpec
val badProvider =
NotifierConfig("vinyldns.core.notifier.NotFoundNotifierProvider", placeholderConfig)
- val load = NotifierLoader.loadAll(List(goodConfig, badProvider), mockUserRepository)
+ val load = NotifierLoader.loadAll(List(goodConfig, badProvider), mockUserRepository, mockGroupRepository)
a[ClassNotFoundException] shouldBe thrownBy(load.unsafeRunSync())
}
@@ -123,7 +123,7 @@ class NotifierLoaderSpec
val exceptionProvider =
NotifierConfig("vinyldns.core.notifier.FailingProvider", placeholderConfig)
- val load = NotifierLoader.loadAll(List(goodConfig, exceptionProvider), mockUserRepository)
+ val load = NotifierLoader.loadAll(List(goodConfig, exceptionProvider), mockUserRepository, mockGroupRepository)
a[IllegalStateException] shouldBe thrownBy(load.unsafeRunSync())
}
diff --git a/modules/core/src/test/scala/vinyldns/core/protobuf/ProtobufConversionsSpec.scala b/modules/core/src/test/scala/vinyldns/core/protobuf/ProtobufConversionsSpec.scala
index 4cdae225d..d72deb78e 100644
--- a/modules/core/src/test/scala/vinyldns/core/protobuf/ProtobufConversionsSpec.scala
+++ b/modules/core/src/test/scala/vinyldns/core/protobuf/ProtobufConversionsSpec.scala
@@ -19,7 +19,7 @@ package vinyldns.core.protobuf
import java.time.Instant
import org.scalatest.{Assertion, OptionValues}
import vinyldns.core.TestRecordSetData.ds
-import vinyldns.core.domain.Fqdn
+import vinyldns.core.domain.{Encrypted, Fqdn}
import vinyldns.core.domain.membership.UserChange.{CreateUser, UpdateUser}
import vinyldns.core.domain.membership.{LockStatus, User, UserChangeType}
import vinyldns.core.domain.record._
@@ -37,7 +37,7 @@ class ProtobufConversionsSpec
with ProtobufConversions
with OptionValues {
- private val zoneConnection = ZoneConnection("name", "keyName", "key", "server")
+ private val zoneConnection = ZoneConnection("name", "keyName", Encrypted("key"), "server")
private val zoneId = "test.zone.id"
@@ -64,8 +64,8 @@ class ProtobufConversionsSpec
private val zone = Zone(
"test.zone.actor.zone",
"test@test.com",
- connection = Some(ZoneConnection("connection.ok", "keyName", "key", "10.1.1.1")),
- transferConnection = Some(ZoneConnection("connection.ok", "keyName", "key", "10.1.1.2")),
+ connection = Some(ZoneConnection("connection.ok", "keyName", Encrypted("key"), "10.1.1.1")),
+ transferConnection = Some(ZoneConnection("connection.ok", "keyName", Encrypted("key"), "10.1.1.2")),
shared = true,
id = zoneId,
acl = zoneAcl,
@@ -270,7 +270,7 @@ class ProtobufConversionsSpec
val pbconn = pb.getConnection
val conn = zn.connection.get
pbconn.getName shouldBe conn.name
- pbconn.getKey shouldBe conn.key
+ pbconn.getKey shouldBe conn.key.value
pbconn.getKeyName shouldBe conn.keyName
pbconn.getPrimaryServer shouldBe conn.primaryServer
} else {
@@ -280,7 +280,7 @@ class ProtobufConversionsSpec
val pbTransConn = pb.getTransferConnection
val transConn = zn.transferConnection.get
pbTransConn.getName shouldBe transConn.name
- pbTransConn.getKey shouldBe transConn.key
+ pbTransConn.getKey shouldBe transConn.key.value
pbTransConn.getKeyName shouldBe transConn.keyName
pbTransConn.getPrimaryServer shouldBe transConn.primaryServer
} else {
@@ -369,7 +369,7 @@ class ProtobufConversionsSpec
"convert from ZoneConnection" in {
val pb = toPB(zoneConnection)
- pb.getKey shouldBe zoneConnection.key
+ pb.getKey shouldBe zoneConnection.key.value
pb.getKeyName shouldBe zoneConnection.keyName
pb.getPrimaryServer shouldBe zoneConnection.primaryServer
pb.getName shouldBe zoneConnection.name
@@ -808,12 +808,12 @@ class ProtobufConversionsSpec
"User conversion" should {
"convert to/from protobuf with user defaults" in {
- val user = User("testName", "testAccess", "testSecret")
+ val user = User("testName", "testAccess", Encrypted("testSecret"))
val pb = toPB(user)
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
pb.hasFirstName shouldBe false
pb.hasLastName shouldBe false
pb.hasEmail shouldBe false
@@ -831,7 +831,7 @@ class ProtobufConversionsSpec
val user = User(
"testName",
"testAccess",
- "testSecret",
+ Encrypted("testSecret"),
firstName = Some("testFirstName"),
lastName = Some("testLastName"),
email = Some("testEmail"),
@@ -841,7 +841,7 @@ class ProtobufConversionsSpec
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
Some(pb.getFirstName) shouldBe user.firstName
Some(pb.getLastName) shouldBe user.lastName
Some(pb.getEmail) shouldBe user.email
@@ -856,12 +856,12 @@ class ProtobufConversionsSpec
}
"convert to/from protobuf with superUser true" in {
- val user = User("testName", "testAccess", "testSecret", isSuper = true)
+ val user = User("testName", "testAccess", Encrypted("testSecret"), isSuper = true)
val pb = toPB(user)
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
pb.hasFirstName shouldBe false
pb.hasLastName shouldBe false
pb.hasEmail shouldBe false
@@ -875,12 +875,12 @@ class ProtobufConversionsSpec
}
"convert to/from protobuf with locked user" in {
- val user = User("testName", "testAccess", "testSecret", lockStatus = LockStatus.Locked)
+ val user = User("testName", "testAccess", Encrypted("testSecret"), lockStatus = LockStatus.Locked)
val pb = toPB(user)
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
pb.hasFirstName shouldBe false
pb.hasLastName shouldBe false
pb.hasEmail shouldBe false
@@ -894,12 +894,12 @@ class ProtobufConversionsSpec
}
"convert to/from protobuf with test user" in {
- val user = User("testName", "testAccess", "testSecret", isTest = true)
+ val user = User("testName", "testAccess", Encrypted("testSecret"), isTest = true)
val pb = toPB(user)
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
pb.hasFirstName shouldBe false
pb.hasLastName shouldBe false
pb.hasEmail shouldBe false
@@ -913,12 +913,12 @@ class ProtobufConversionsSpec
}
"convert to/from protobuf with supportAdmin true" in {
- val user = User("testName", "testAccess", "testSecret", isSupport = true)
+ val user = User("testName", "testAccess", Encrypted("testSecret"), isSupport = true)
val pb = toPB(user)
pb.getUserName shouldBe user.userName
pb.getAccessKey shouldBe user.accessKey
- pb.getSecretKey shouldBe user.secretKey
+ pb.getSecretKey shouldBe user.secretKey.value
pb.hasFirstName shouldBe false
pb.hasLastName shouldBe false
pb.hasEmail shouldBe false
@@ -934,7 +934,7 @@ class ProtobufConversionsSpec
"User change conversion" should {
"convert to/from protobuf for CreateUser" in {
- val user = User("createUser", "createUserAccess", "createUserSecret")
+ val user = User("createUser", "createUserAccess", Encrypted("createUserSecret"))
val createChange = CreateUser(user, "createUserId", user.created)
val pb = toPb(createChange)
@@ -943,7 +943,7 @@ class ProtobufConversionsSpec
new User(
pb.getNewUser.getUserName,
pb.getNewUser.getAccessKey,
- pb.getNewUser.getSecretKey,
+ Encrypted(pb.getNewUser.getSecretKey),
created = user.created,
id = user.id
) shouldBe user
@@ -956,7 +956,7 @@ class ProtobufConversionsSpec
}
"convert to/from protobuf for UpdateUser" in {
- val oldUser = User("updateUser", "updateUserAccess", "updateUserSecret")
+ val oldUser = User("updateUser", "updateUserAccess", Encrypted("updateUserSecret"))
val newUser = oldUser.copy(userName = "updateUserNewName")
val updateChange = UpdateUser(newUser, "createUserId", newUser.created, oldUser)
val pb = toPb(updateChange)
@@ -966,7 +966,7 @@ class ProtobufConversionsSpec
new User(
pb.getNewUser.getUserName,
pb.getNewUser.getAccessKey,
- pb.getNewUser.getSecretKey,
+ Encrypted(pb.getNewUser.getSecretKey),
created = newUser.created,
id = newUser.id
) shouldBe newUser
@@ -977,7 +977,7 @@ class ProtobufConversionsSpec
new User(
pb.getOldUser.getUserName,
pb.getOldUser.getAccessKey,
- pb.getOldUser.getSecretKey,
+ Encrypted(pb.getOldUser.getSecretKey),
created = oldUser.created,
id = oldUser.id
) shouldBe oldUser
diff --git a/modules/core/src/test/scala/vinyldns/core/route/MonitorSpec.scala b/modules/core/src/test/scala/vinyldns/core/route/MonitorSpec.scala
index 42ef31a28..eb94af8cf 100644
--- a/modules/core/src/test/scala/vinyldns/core/route/MonitorSpec.scala
+++ b/modules/core/src/test/scala/vinyldns/core/route/MonitorSpec.scala
@@ -116,7 +116,7 @@ class MonitorSpec
val msgCaptor = ArgumentCaptor.forClass(classOf[String])
val errorCaptor = ArgumentCaptor.forClass(classOf[String])
verify(traitTest.logger, times(1)).info(msgCaptor.capture())
- verify(traitTest.logger, times(1)).error(errorCaptor.capture(), any(classOf[Throwable]))
+ verify(traitTest.logger, times(1)).error(errorCaptor.capture())
msgCaptor.getValue shouldBe "Starting timeSomethingBad"
errorCaptor.getValue should include("Finished timeSomethingBad; success=false; duration=")
diff --git a/modules/docs/src/main/mdoc/api/create-group.md b/modules/docs/src/main/mdoc/api/create-group.md
index a535e74ed..6334cecf4 100644
--- a/modules/docs/src/main/mdoc/api/create-group.md
+++ b/modules/docs/src/main/mdoc/api/create-group.md
@@ -6,7 +6,7 @@ section: "api"
# Create Group
-Creates a Group in VinylDNS
+Creates a Group in VinylDNS.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/create-recordset.md b/modules/docs/src/main/mdoc/api/create-recordset.md
index ba6781577..91770beb9 100644
--- a/modules/docs/src/main/mdoc/api/create-recordset.md
+++ b/modules/docs/src/main/mdoc/api/create-recordset.md
@@ -6,7 +6,7 @@ section: "api"
# Create RecordSet
-Creates a RecordSet in a specified zone
+Creates a RecordSet in a specified zone.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/create-zone.md b/modules/docs/src/main/mdoc/api/create-zone.md
index c97c3da71..0232f36b0 100644
--- a/modules/docs/src/main/mdoc/api/create-zone.md
+++ b/modules/docs/src/main/mdoc/api/create-zone.md
@@ -7,7 +7,7 @@ section: "api"
# Create Zone
Connects user to an existing zone. User must be a member of the group that has access to the zone. Connection info is optional,
-if no info is provided the default VinylDNS connections will be used
+if no info is provided the default VinylDNS connections will be used.
#### HTTP REQUEST
@@ -15,7 +15,7 @@ if no info is provided the default VinylDNS connections will be used
#### HTTP REQUEST PARAMS
-**zone fields** - adminGroupId, name, and email are required - refer to [zone model](zone-model.html) |
+**Zone fields** - adminGroupId, name, and email are required. Refer to [zone model](zone-model.html).
#### EXAMPLE HTTP REQUEST
```json
@@ -40,8 +40,8 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-status | string | Status of zone change |
-zone | map | Refer to [zone model](zone-model.html) |
+status | string | Status of zone change |
+zone | map | Refer to [zone model](zone-model.html) |
created | string | The timestamp (UTC) the change was initiated |
changeType | string | Type of change requested (Create, Update, Sync, Delete); in this case Create |
userId | string | The user id that initiated the change |
diff --git a/modules/docs/src/main/mdoc/api/delete-group.md b/modules/docs/src/main/mdoc/api/delete-group.md
index 30cb3a1db..a3ca53edf 100644
--- a/modules/docs/src/main/mdoc/api/delete-group.md
+++ b/modules/docs/src/main/mdoc/api/delete-group.md
@@ -6,7 +6,7 @@ section: "api"
# Delete Group
-Deletes a Group in VinylDNS
+Deletes a Group in VinylDNS.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/delete-recordset.md b/modules/docs/src/main/mdoc/api/delete-recordset.md
index aa4d05c7b..c6a19cce6 100644
--- a/modules/docs/src/main/mdoc/api/delete-recordset.md
+++ b/modules/docs/src/main/mdoc/api/delete-recordset.md
@@ -6,7 +6,7 @@ section: "api"
# Delete RecordSet
-Delete a RecordSet in a specified zone
+Delete a RecordSet in a specified zone.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/get-group-change.md b/modules/docs/src/main/mdoc/api/get-group-change.md
new file mode 100644
index 000000000..6869ca3f2
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/get-group-change.md
@@ -0,0 +1,85 @@
+---
+layout: docs
+title: "Get Group Change"
+section: "api"
+---
+
+# Get Group Change
+
+Retrieves a group change given the group change ID.
+
+#### HTTP REQUEST
+
+> GET /groups/change/{groupChangeId}
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - The group change is returned in the response body
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+404 | **Not Found** - The group change was not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ----------------- | ------------- | :---------- |
+newGroup | map | The new group as a result of the change. Refer to [Membership Model](membership-model.html) |
+oldGroup | map | The old group before the change. Refer to [Membership Model](membership-model.html) |
+created | string | Millisecond timestamp that change was created
+userId | string | User Id of user who made the change |
+id | string | Id of the group change |
+userName | string | Username of user who made the change |
+groupChangeMessage | string | The description of the changes made to the group |
+changeType | string | The type change, either Create, Update, or Delete |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "newGroup": {
+ "id": "7f420e07-3043-46f3-97e2-8d1ac47d08db",
+ "name": "test-group",
+ "email": "test@test.com",
+ "created": "2023-09-29T06:37:04Z",
+ "status": "Active",
+ "members": [
+ {
+ "id": "d4f47898-7a41-4b0d-ba18-de4788b9f102"
+ },
+ {
+ "id": "6c4bfd61-d1c6-426e-8123-6f7c28f89d2d"
+ }
+ ],
+ "admins": [
+ {
+ "id": "d4f47898-7a41-4b0d-ba18-de4788b9f102"
+ }
+ ]
+ },
+ "changeType": "Update",
+ "userId": "d4f47898-7a41-4b0d-ba18-de4788b9f102",
+ "oldGroup": {
+ "id": "7f420e07-3043-46f3-97e2-8d1ac47d08db",
+ "name": "test-group",
+ "email": "test@test.com",
+ "created": "2023-09-29T06:37:04Z",
+ "status": "Active",
+ "members": [
+ {
+ "id": "d4f47898-7a41-4b0d-ba18-de4788b9f102"
+ }
+ ],
+ "admins": [
+ {
+ "id": "d4f47898-7a41-4b0d-ba18-de4788b9f102"
+ }
+ ]
+ },
+ "id": "a3227632-a166-407e-8a74-c0624d967b58",
+ "created": "2023-09-29T06:37:12.061Z",
+ "userName": "testuser",
+ "groupChangeMessage": "Group member/s with user name/s 'hermes' added."
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/get-group.md b/modules/docs/src/main/mdoc/api/get-group.md
index fa9725161..462737b65 100644
--- a/modules/docs/src/main/mdoc/api/get-group.md
+++ b/modules/docs/src/main/mdoc/api/get-group.md
@@ -6,7 +6,7 @@ section: "api"
# Get Group
-Gets a group that you are a part of
+Gets a group that you are a part of.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/get-recordset-change-history.md b/modules/docs/src/main/mdoc/api/get-recordset-change-history.md
new file mode 100644
index 000000000..693d1e8e5
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/get-recordset-change-history.md
@@ -0,0 +1,145 @@
+---
+layout: docs
+title: "Get RecordSet Change History"
+section: "api"
+---
+
+# Get RecordSet Change History
+
+Gets the history of all the changes that has been made to a recordset
+
+#### HTTP REQUEST
+
+> GET /recordsetchange/history?zoneId={zoneId}&fqdn={recordsetFqdn}&recordType={recordsetType}&startFrom={response.nextId}&maxItems={1 - 100}
+
+#### HTTP REQUEST PARAMS
+
+name | type | required? | description |
+ ------------ | ------------- | ----------- | :---------- |
+zoneId | string | yes | The id of the zone where the recordset is present |
+fqdn | string | yes | The fqdn of the recordset whose history will be returned |
+recordType | string | yes | The record type of the recordset |
+startFrom | int | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - The record set change history is returned in the response body
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+404 | **Not Found** - The history for provided recordset was not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ------------------- | ------------- | :---------- |
+zoneId | string | The id of the zone where the recordset is present |
+recordSetChanges | array of recordset changes | Array of recordset changes sorted by created time in descending order |
+startFrom | int | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | int | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
+maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "zoneId": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "recordSetChanges": [
+ {
+ "zone": {
+ "name": "ok.",
+ "email": "test@test.com",
+ "status": "Active",
+ "created": "2023-09-27T07:41:12Z",
+ "id": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "account": "system",
+ "shared": false,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "7e56dfdf-df1b-4a39-a3e1-2977db13a1fd",
+ "latestSync": "2023-09-27T07:41:13Z",
+ "isTest": false
+ },
+ "recordSet": {
+ "type": "A",
+ "zoneId": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "name": "ok.",
+ "ttl": 38400,
+ "status": "Active",
+ "created": "2023-09-27T07:41:28Z",
+ "updated": "2023-09-27T07:41:28Z",
+ "records": [
+ {
+ "address": "5.5.5.6"
+ }
+ ],
+ "id": "cda5f6e1-b103-41ad-af7d-013a4379c591",
+ "account": "system"
+ },
+ "userId": "83bd9eda-145d-4799-98fb-20d0bf1ed1e1",
+ "changeType": "Update",
+ "status": "Complete",
+ "created": "2023-09-27T07:41:28Z",
+ "updates": {
+ "type": "A",
+ "zoneId": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "name": "ok.",
+ "ttl": 38400,
+ "status": "Active",
+ "created": "2023-09-27T07:41:13Z",
+ "records": [
+ {
+ "address": "5.5.5.5"
+ }
+ ],
+ "id": "cda5f6e1-b103-41ad-af7d-013a4379c591",
+ "account": "system"
+ },
+ "id": "55994cb7-9dcb-41ff-980c-44385d1d2cfa",
+ "userName": "professor"
+ },
+ {
+ "zone": {
+ "name": "ok.",
+ "email": "test@test.com",
+ "status": "Syncing",
+ "created": "2023-09-27T07:41:12Z",
+ "id": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "account": "system",
+ "shared": false,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "7e56dfdf-df1b-4a39-a3e1-2977db13a1fd",
+ "isTest": false
+ },
+ "recordSet": {
+ "type": "A",
+ "zoneId": "56b03014-7f68-4a9b-b5b6-c0e6a212992d",
+ "name": "ok.",
+ "ttl": 38400,
+ "status": "Active",
+ "created": "2023-09-27T07:41:13Z",
+ "records": [
+ {
+ "address": "5.5.5.5"
+ }
+ ],
+ "id": "cda5f6e1-b103-41ad-af7d-013a4379c591",
+ "account": "system"
+ },
+ "userId": "83bd9eda-145d-4799-98fb-20d0bf1ed1e1",
+ "changeType": "Create",
+ "status": "Complete",
+ "created": "2023-09-27T07:41:13Z",
+ "systemMessage": "Change applied via zone sync",
+ "id": "3575868a-ccde-40ec-aaeb-b33cb8410eaf",
+ "userName": "professor"
+ }
+ ],
+ "maxItems": 100
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/get-recordset-change.md b/modules/docs/src/main/mdoc/api/get-recordset-change.md
index a382a4fed..00c30f5fd 100644
--- a/modules/docs/src/main/mdoc/api/get-recordset-change.md
+++ b/modules/docs/src/main/mdoc/api/get-recordset-change.md
@@ -7,7 +7,7 @@ section: "api"
# Get RecordSet Change
RecordSet changes (Create, Update, Delete) are not immediately applied to the DNS backend; they are queued up for processing. Most changes are applied within a few seconds.
-When you submit a change for processing, the response is a Change model. You can use the information in that change model in order to poll for the status of the change until it completes (status = Complete) or fails (status = Failed)
+When you submit a change for processing, the response is a Change model. You can use the information in that change model in order to poll for the status of the change until it completes (status = Complete) or fails (status = Failed).
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/get-recordset-count.md b/modules/docs/src/main/mdoc/api/get-recordset-count.md
new file mode 100644
index 000000000..4ad612d74
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/get-recordset-count.md
@@ -0,0 +1,36 @@
+---
+layout: docs
+title: "Get RecordSet Count"
+section: "api"
+---
+
+# Get RecordSet Count
+
+Gets the count of total recordsets in a specified zone.
+
+#### HTTP REQUEST
+
+> GET /zones/{zoneId}/recordsetcount
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - The total record set count in a zone is returned |
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+404 | **Not Found** - The zone with the id specified was not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ------------ | ------------- | :---------- |
+count | integer | Total count of recordsets in a zone |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "count": 10
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/get-recordset.md b/modules/docs/src/main/mdoc/api/get-recordset.md
index 5d1a10e46..a09fa7060 100644
--- a/modules/docs/src/main/mdoc/api/get-recordset.md
+++ b/modules/docs/src/main/mdoc/api/get-recordset.md
@@ -6,7 +6,7 @@ section: "api"
# Get RecordSet
-Gets a RecordSet in a specified zone
+Gets a RecordSet in a specified zone.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/get-user.md b/modules/docs/src/main/mdoc/api/get-user.md
index 8ad08cfd9..b601f860c 100644
--- a/modules/docs/src/main/mdoc/api/get-user.md
+++ b/modules/docs/src/main/mdoc/api/get-user.md
@@ -6,7 +6,7 @@ section: "api"
# Get User
-Gets a user corresponding to the given identifier (user ID or username)
+Gets a user corresponding to the given identifier (user ID or username).
#### HTTP REQUEST
@@ -26,12 +26,19 @@ Gets a user corresponding to the given identifier (user ID or username)
|----------|--------|:-------------------------|
| id | string | Unique UUID of the user |
| userName | string | The username of the user |
+| groupId | Array of groupId's | The Group ID's of the user |
+
#### EXAMPLE RESPONSE
```json
{
"id": "ok",
- "userName": "ok"
+ "userName": "ok",
+ "groupId" : [
+ {
+ "id": "ok-group"
+ }
+ ]
}
```
diff --git a/modules/docs/src/main/mdoc/api/get-valid-email-domains.md b/modules/docs/src/main/mdoc/api/get-valid-email-domains.md
new file mode 100644
index 000000000..2f6838b8d
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/get-valid-email-domains.md
@@ -0,0 +1,36 @@
+---
+layout: docs
+title: "Get Valid Email Domains"
+section: "api"
+---
+
+# Get Valid Email Domains
+
+Gets a list of valid email domains which are allowed while entering groups and zones email.
+
+#### HTTP REQUEST
+
+> GET groups/valid/domains
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - The valid email domains are returned in the response body |
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+404 | **Not Found** - The valid email domains are not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+type | description |
+| ------------- | :---------- |
+| Array of string | The list of all valid email domains |
+
+#### EXAMPLE RESPONSE
+
+```json
+[
+ "gmail.com",
+ "test.com"
+]
+```
diff --git a/modules/docs/src/main/mdoc/api/get-zone-by-id.md b/modules/docs/src/main/mdoc/api/get-zone-by-id.md
index 7d209d766..626c4f18d 100644
--- a/modules/docs/src/main/mdoc/api/get-zone-by-id.md
+++ b/modules/docs/src/main/mdoc/api/get-zone-by-id.md
@@ -6,7 +6,7 @@ section: "api"
# Get Zone by ID
-Retrieves a zone with the matching zone ID
+Retrieves a zone with the matching zone ID.
#### HTTP REQUEST
@@ -25,7 +25,7 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-zone | map | refer to [zone model](zone-model.html) |
+zone | map | Refer to [zone model](zone-model.html) |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/get-zone-by-name.md b/modules/docs/src/main/mdoc/api/get-zone-by-name.md
index 3b8f76c95..00e297443 100644
--- a/modules/docs/src/main/mdoc/api/get-zone-by-name.md
+++ b/modules/docs/src/main/mdoc/api/get-zone-by-name.md
@@ -6,7 +6,7 @@ section: "api"
# Get Zone by Name
-Retrieves a zone with the matching zone name
+Retrieves a zone with the matching zone name.
#### HTTP REQUEST
@@ -25,7 +25,7 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-zone | map | refer to [zone model](zone-model.html) |
+zone | map | Refer to [zone model](zone-model.html) |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/list-batchchanges.md b/modules/docs/src/main/mdoc/api/list-batchchanges.md
index fc18ef369..bca269f44 100644
--- a/modules/docs/src/main/mdoc/api/list-batchchanges.md
+++ b/modules/docs/src/main/mdoc/api/list-batchchanges.md
@@ -12,7 +12,7 @@ The max number of batch changes that are returned from a single request has been
#### HTTP REQUEST
-> GET zones/batchrecordchanges?startFrom={response.nextId}&maxItems={1-100}&ignoreAccess={true | false}&approval_status={BatchChangeApprovalStatus}
+> GET zones/batchrecordchanges?startFrom={response.nextId}&maxItems={1-100}&ignoreAccess={true | false}&approvalStatus={batchChangeApprovalStatus}&userName={submitterUserName}&dateTimeRangeStart={dateTimeRangeStart}&dateTimeRangeEnd={dateTimeRangeEnd}
#### HTTP REQUEST PARAMS
@@ -22,6 +22,9 @@ startFrom | int | no | In order to advance through pages
maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
ignoreAccess | boolean | no | Flag determining whether to retrieve only batch changes made by calling user or to retrieve all changes. Only affects system administrators (ie. support and super users). Defaults to `false` if not provided. |
approvalStatus| BatchChangeApprovalStatus| no | Filter batch changes based on approval status. Can be one of **AutoApproved**, **PendingReview**, **ManuallyApproved**, **Rejected**, or **Cancelled**. |
+userName | string | no | Filter batch changes based on submitter user name |
+dateTimeRangeStart | string | no | Start date time value to filter batch changes based on date time range |
+dateTimeRangeEnd | string | no | End date time value to filter batch changes based on date time range |
#### HTTP RESPONSE TYPES
@@ -42,6 +45,9 @@ nextId | int | `startFrom` parameter of next page request, will n
maxItems | integer | `maxItems` sent in request, default is 100. |
ignoreAccess | boolean | `ignoreAccess` sent in request, default is `false`. |
approvalStatus | BatchChangeApprovalStatus | `approvalStatus` sent in request, will not be returned if not provided. |
+userName | string | `userName` sent in request, will not be returned if not provided. |
+dateTimeRangeStart | string | `dateTimeRangeStart` sent in request, will not be returned if not provided. |
+dateTimeRangeEnd | string | `dateTimeRangeEnd` sent in request, will not be returned if not provided. |
##### BatchChangeSummary
diff --git a/modules/docs/src/main/mdoc/api/list-deleted-zones.md b/modules/docs/src/main/mdoc/api/list-deleted-zones.md
new file mode 100644
index 000000000..9955cd893
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/list-deleted-zones.md
@@ -0,0 +1,107 @@
+---
+layout: docs
+title: "List Abandoned Zones"
+section: "api"
+---
+
+# List Abandoned Zones
+
+Retrieves the list of deleted zones a user has access to. The zone name is only sorted alphabetically.
+
+#### HTTP REQUEST
+
+> GET /zones/deleted/changes?nameFilter={yoursearchhere}&startFrom={response.nextId}&maxItems={1 - 100}&ignoreAccess={true|false}
+
+#### HTTP REQUEST PARAMS
+
+name | type | required? | description |
+ ------------ | ------------- | ----------- | :---------- |
+nameFilter | string | no | Characters that are part of the deleted zone name to search for. The wildcard character `*` is supported, for example `www*`. Omit the wildcard character when searching for an exact deleted zone name. |
+startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
+ignoreAccess | boolean | no | If false, returns only zones the requesting user owns or has ACL access to. If true, returns zones in the system, regardless of ownership. Defaults to false if not provided. |
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - The deleted zones and search info are returned in response body |
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ------------ | ------------- | :---------- |
+zonesDeletedInfo | Array of [Deleted Zones](zone-model.html#zone-attributes) | An array of the deleted zones found. The zones are sorted alphabetically by zone name. |
+startFrom | string | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | *any* | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present.|
+maxItems | int | The maxItems parameter that was sent in the HTTP request. This will be 100 if not sent. |
+ignoreAccess | boolean | The ignoreAccess parameter that was sent in the HTTP request. This will be false if not sent. |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "zonesDeletedInfo": [
+ {
+ "zoneChange": {
+ "zone": {
+ "name": "dummy.",
+ "email": "test@test.com",
+ "status": "Deleted",
+ "created": "2023-09-26T09:32:08Z",
+ "updated": "2023-09-26T09:32:24Z",
+ "id": "01975877-ff13-4605-a940-533b87718726",
+ "account": "system",
+ "shared": false,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "7d034091-d14f-40fa-a42f-5264a71fe6af",
+ "latestSync": "2023-09-26T09:32:08Z",
+ "isTest": false
+ },
+ "userId": "4b2f14fc-d57a-4ea3-88ee-602d5cfb533c",
+ "changeType": "Delete",
+ "status": "Synced",
+ "created": "2023-09-26T09:32:24Z",
+ "id": "ff28aa95-0b35-469e-8fef-e2fb97b9e247"
+ },
+ "adminGroupName": "duGroup",
+ "userName": "professor",
+ "accessLevel": "NoAccess"
+ },
+ {
+ "zoneChange": {
+ "zone": {
+ "name": "ok.",
+ "email": "test@test.com",
+ "status": "Deleted",
+ "created": "2023-09-25T14:16:56Z",
+ "updated": "2023-09-26T09:19:27Z",
+ "id": "96b85fed-61d4-41bf-ac81-fdbfe3d1c037",
+ "account": "system",
+ "shared": false,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "1f938110-cd0d-4670-8c64-5f53f1cce2f1",
+ "latestSync": "2023-09-25T14:16:57Z",
+ "isTest": false
+ },
+ "userId": "8628a36b-9302-41d1-bd0a-7610cc964086",
+ "changeType": "Delete",
+ "status": "Synced",
+ "created": "2023-09-26T09:19:27Z",
+ "id": "8925f690-4586-465d-8309-c3d4ca6dd420"
+ },
+ "adminGroupName": "name",
+ "userName": "fry",
+ "accessLevel": "Delete"
+ }
+ ],
+ "maxItems": 100,
+ "ignoreAccess": true
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/list-group-activity.md b/modules/docs/src/main/mdoc/api/list-group-activity.md
index 191e7d044..408663067 100644
--- a/modules/docs/src/main/mdoc/api/list-group-activity.md
+++ b/modules/docs/src/main/mdoc/api/list-group-activity.md
@@ -6,7 +6,7 @@ section: "api"
# List Group Activity
-Retrieves a list of group activity
+Retrieves a list of group activity.
#### HTTP REQUEST
@@ -16,7 +16,7 @@ Retrieves a list of group activity
name | type | required? | description |
------------ | ------------- | ----------- | :---------- |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | integer | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -31,73 +31,89 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-changes | Array of Group Changes | refer to [Group Change](#group-change) |
-startFrom | *any* | startFrom sent in request, will not be returned if not provided |
-nextId | *any* | nextId, used as startFrom parameter of next page request, will not be returned if activity is exhausted |
+changes | Array of Group Changes | refer to [Group Change](#group-change) |
+startFrom | integer | startFrom sent in request, will not be returned if not provided |
+nextId | integer | nextId, used as startFrom parameter of next page request, will not be returned if activity is exhausted |
maxItems | integer | maxItems sent in request, default is 100 |
#### GROUP CHANGE ATTRIBUTES
-name | type | description |
- ------------ | ------------- | :---------- |
-newGroup | map | The new group as a result of the change, refer to [Membership Model](membership-model.html) |
-oldGroup | map | The old group before the change, refer to [Membership Model](membership-model.html) |
-created | string | Millisecond timestamp that change was created
-userId | string | User Id of user who made the change |
-changeType | string | The type change, either Create, Update, or Delete |
+name | type | description |
+ ----------------- | ------------- | :---------- |
+newGroup | map | The new group as a result of the change. Refer to [Membership Model](membership-model.html) |
+oldGroup | map | The old group before the change. Refer to [Membership Model](membership-model.html) |
+created | string | Millisecond timestamp that change was created
+userId | string | User Id of user who made the change |
+id | string | Id of the group change |
+userName | string | Username of user who made the change |
+groupChangeMessage | string | The description of the changes made to the group |
+changeType | string | The type change, either Create, Update, or Delete |
#### EXAMPLE RESPONSE
```json
{
- "maxItems": 100,
"changes": [
{
"newGroup": {
+ "id": "6edb08fe-8179-4e18-aa08-2acc1785c364",
+ "name": "test-group",
+ "email": "test@test.com",
+ "created": "2024-02-22T07:32:51Z",
"status": "Active",
- "name": "test-list-group-activity-max-item-success",
- "created": "2017-03-02T18:49:58Z",
- "id": "1555bac7-0343-4d11-800f-955afb481818",
- "admins": [
- {
- "id": "ok"
- }
- ],
"members": [
{
- "id": "dummy199"
+ "id": "6a8545e7-cbab-47c9-8aa2-c56e413c44b6"
},
{
- "id": "ok"
+ "id": "6c83a035-cc1b-4d94-acd6-bb2da351edca"
+ },
+ {
+ "id": "864f7002-e48e-451c-9909-50567ecdc1a5"
}
],
- "email": "test@test.com"
+ "admins": [
+ {
+ "id": "6a8545e7-cbab-47c9-8aa2-c56e413c44b6"
+ }
+ ]
},
- "created": "1488480605378",
- "userId": "some-user",
"changeType": "Update",
+ "userId": "6a8545e7-cbab-47c9-8aa2-c56e413c44b6",
"oldGroup": {
+ "id": "6edb08fe-8179-4e18-aa08-2acc1785c364",
+ "name": "test-group",
+ "email": "test@test.com",
+ "created": "2024-02-22T07:32:51Z",
"status": "Active",
- "name": "test-list-group-activity-max-item-success",
- "created": "2017-03-02T18:49:58Z",
- "id": "1555bac7-0343-4d11-800f-955afb481818",
- "admins": [
- {
- "id": "ok"
- }
- ],
"members": [
{
- "id": "dummy198"
+ "id": "6a8545e7-cbab-47c9-8aa2-c56e413c44b6"
},
{
- "id": "ok"
+ "id": "6c83a035-cc1b-4d94-acd6-bb2da351edca"
+ },
+ {
+ "id": "864f7002-e48e-451c-9909-50567ecdc1a5"
}
],
- "email": "test@test.com"
+ "admins": [
+ {
+ "id": "6a8545e7-cbab-47c9-8aa2-c56e413c44b6"
+ },
+ {
+ "id": "6c83a035-cc1b-4d94-acd6-bb2da351edca"
+ }
+ ]
},
- "id": "11abb88b-c47d-469b-bc2d-6656e00711cf"
+ "id": "1c1151e9-099f-4cb8-aa24-bf43d21e5fd5",
+ "created": "2024-02-22T07:33:09.262Z",
+ "userName": "professor",
+ "groupChangeMessage": "Group admin/s with user name/s 'fry' removed."
}
- ]
+ ],
+ "startFrom": 1,
+ "nextId": 2,
+ "maxItems": 1
}
```
diff --git a/modules/docs/src/main/mdoc/api/list-group-admins.md b/modules/docs/src/main/mdoc/api/list-group-admins.md
index a2b68c48e..1a75eca5d 100644
--- a/modules/docs/src/main/mdoc/api/list-group-admins.md
+++ b/modules/docs/src/main/mdoc/api/list-group-admins.md
@@ -6,7 +6,7 @@ section: "api"
# List Group Admins
-Retrieves a group's admins
+Retrieves a group's admins.
#### HTTP REQUEST
@@ -24,7 +24,7 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-admins | Array of Users | refer to [membership model](membership-model.html) |
+admins | Array of Users | Refer to [membership model](membership-model.html) |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/list-group-members.md b/modules/docs/src/main/mdoc/api/list-group-members.md
index e864c7b11..27383b139 100644
--- a/modules/docs/src/main/mdoc/api/list-group-members.md
+++ b/modules/docs/src/main/mdoc/api/list-group-members.md
@@ -6,7 +6,7 @@ section: "api"
# List Group Members
-Retrieves a list of group members
+Retrieves a list of group members.
#### HTTP REQUEST
@@ -16,7 +16,7 @@ Retrieves a list of group members
name | type | required? | description |
------------ | ------------- | ----------- | :---------- |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -29,12 +29,12 @@ Code | description |
#### HTTP RESPONSE ATTRIBUTES
-name | type | description |
- ------------ | ------------- | :---------- |
-members | Array of Users | refer to [membership model](membership-model.html), these Users will also include an isAdmin attribute |
-startFrom | *any* | startFrom sent in request, will not be returned if not provided |
-nextId | *any* | nextId, used as startFrom parameter of next page request, will not be returned if members are exhausted |
-maxItems | integer | maxItems sent in request, default is 100 |
+name | type | description |
+ ------------ | ------------- | :---------- |
+members | Array of Users | refer to [membership model](membership-model.html), these Users will also include an isAdmin attribute |
+startFrom | string | startFrom sent in request, will not be returned if not provided |
+nextId | string | nextId, used as startFrom parameter of next page request, will not be returned if members are exhausted |
+maxItems | integer | maxItems sent in request, default is 100 |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/list-groups.md b/modules/docs/src/main/mdoc/api/list-groups.md
index 43dadac01..6cfad95ca 100644
--- a/modules/docs/src/main/mdoc/api/list-groups.md
+++ b/modules/docs/src/main/mdoc/api/list-groups.md
@@ -6,20 +6,21 @@ section: "api"
# List Groups
-Retrieves a list of groups that you are a part of
+Retrieves a list of groups that you are a part of.
#### HTTP REQUEST
-> GET /groups?startFrom={response.nextId}&maxItems={1 - 100}&groupNameFilter={filter}&ignoreAccess={true | false}
+> GET /groups?startFrom={response.nextId}&maxItems={1 - 100}&groupNameFilter={filter}&ignoreAccess={true | false}&abridged={true | false}
#### HTTP REQUEST PARAMS
-name | type | required? | description |
- ------------ | ------------- | ----------- | :---------- |
-groupNameFilter | string | no | One or more characters contained in the name of the group set to search for. For example `TP`. This is a contains search only, no wildcards or regular expressions are supported |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
-maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
-ignoreAccess | boolean | no | If false, returns only groups the requesting user is a member of. If true, returns groups in the system, regardless of membership. Defaults to false if not provided. Super and support admin see all groups regardless of this value. |
+name | type | required? | description |
+ ------------ | ------------- | ----------- | :---------- |
+groupNameFilter | string | no | One or more characters contained in the name of the group set to search for. For example `TP`. This is a contains search only, no wildcards or regular expressions are supported |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
+ignoreAccess | boolean | no | If false, returns only groups the requesting user is a member of. If true, returns groups in the system, regardless of membership. Defaults to false if not provided. Super and support admin see all groups regardless of this value. |
+abridged | boolean | no | If false, returns all the group details. If true, returns an abridged version of group details. Defaults to false if not provided. |
#### HTTP RESPONSE TYPES
@@ -30,14 +31,14 @@ Code | description |
#### HTTP RESPONSE ATTRIBUTES
-name | type | description |
- ------------ | ------------- | :---------- |
-groups | Array of Groups | refer to [membership model](membership-model.html) |
-groupNameFilter | string | name filter sent in request |
-startFrom | *any* | startFrom sent in request, will not be returned if not provided |
-nextId | *any* | nextId, used as startFrom parameter of next page request, will not be returned if groups are exhausted |
-maxItems | integer | maxItems sent in request, default is 100 |
-ignoreAccess | boolean | The ignoreAccess parameter that was sent in the HTTP request. This will be false if not sent. |
+name | type | description |
+ ------------ | ------------- | :---------- |
+groups | Array of Groups | refer to [membership model](membership-model.html) |
+groupNameFilter | string | name filter sent in request |
+startFrom | string | startFrom sent in request, will not be returned if not provided |
+nextId | string | nextId, used as startFrom parameter of next page request, will not be returned if groups are exhausted |
+maxItems | integer | maxItems sent in request, default is 100 |
+ignoreAccess | boolean | The ignoreAccess parameter that was sent in the HTTP request. This will be false if not sent. |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/list-recordset-change-failures.md b/modules/docs/src/main/mdoc/api/list-recordset-change-failures.md
new file mode 100644
index 000000000..3fc2391fb
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/list-recordset-change-failures.md
@@ -0,0 +1,97 @@
+---
+layout: docs
+title: "List RecordSet Change Failures"
+section: "api"
+---
+
+# List Recordset Change Failures
+
+Retrieves a list of RecordSet changes that failed in a zone.
+
+#### HTTP REQUEST
+
+> GET metrics/health/zones/{zoneId}/recordsetchangesfailure?startFrom={response.nextId}&maxItems={1 - 100}
+
+#### HTTP REQUEST PARAMS
+
+name | type | required? | description |
+ ------------ | ------------- | ----------- | :---------- |
+startFrom | int | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **OK** - the list of failed recordset changes are returned in response body |
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+404 | **Not Found** - Zone not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ------------ | ------------- | :---------- |
+failedRecordSetChanges | array of failed recordset changes | array of recordset changes sorted by created time in descending order |
+startFrom | int | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | int | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
+maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "failedRecordSetChanges": [
+ {
+ "zone": {
+ "name": "ok.",
+ "email": "test@test.com",
+ "status": "Active",
+ "created": "2020-11-17T18:50:46Z",
+ "updated": "2023-09-28T13:51:30Z",
+ "id": "fbf7a440-891c-441a-ad09-e1cbc861fda1",
+ "account": "system",
+ "shared": true,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "7611734a-8409-4827-9e8b-960d115b9f9c",
+ "scheduleRequestor": "testuser",
+ "latestSync": "2023-09-28T13:51:30Z",
+ "isTest": true,
+ "backendId": "func-test-backend"
+ },
+ "recordSet": {
+ "type": "DS",
+ "zoneId": "fbf7a440-891c-441a-ad09-e1cbc861fda1",
+ "name": "ns-test-2",
+ "ttl": 7200,
+ "status": "Inactive",
+ "created": "2023-09-28T16:44:47Z",
+ "updated": "2023-09-28T16:44:47Z",
+ "records": [
+ {
+ "keytag": 1,
+ "algorithm": 3,
+ "digesttype": 1,
+ "digest": "01"
+ }
+ ],
+ "id": "a40de44e-1b13-4cfe-9485-79749bd360fa",
+ "account": "system",
+ "ownerGroupId": "7611734a-8409-4827-9e8b-960d115b9f9c"
+ },
+ "userId": "6741d4df-81b7-40bd-9856-896c730c189b",
+ "changeType": "Create",
+ "status": "Failed",
+ "created": "2023-09-28T16:44:47Z",
+ "systemMessage": "Failed applying update to DNS for change 8094c9a9-d279-4847-81f4-de08f2454ff1:ns-test-2: Format Error: the server was unable to interpret the query: ;; ->>HEADER<<- opcode: UPDATE, status: FORMERR, id: 41792\n;; flags: qr ; qd: 1 an: 0 au: 0 ad: 0 \n;; TSIG invalid\n;; ZONE:\n;;\tok., type = SOA, class = IN\n\n;; PREREQUISITES:\n\n;; UPDATE RECORDS:\n\n;; ADDITIONAL RECORDS:\n\n;; Message size: 20 bytes",
+ "id": "8094c9a9-d279-4847-81f4-de08f2454ff1",
+ "singleBatchChangeIds": []
+ }
+ ],
+ "nextId": 0,
+ "startFrom": 0,
+ "maxItems": 100
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/list-recordset-changes.md b/modules/docs/src/main/mdoc/api/list-recordset-changes.md
index ebd5c0397..38ae1cd74 100644
--- a/modules/docs/src/main/mdoc/api/list-recordset-changes.md
+++ b/modules/docs/src/main/mdoc/api/list-recordset-changes.md
@@ -7,9 +7,9 @@ section: "api"
# List RecordSet Changes
RecordSet changes (Create, Update, Delete) are not immediately applied to the DNS backend; they are queued up for processing. Most changes are applied within a few seconds.
-When you submit a change for processing, the response is a Change model. You can use the information in that change model in order to poll for the status of the change until it completes (status = Complete) or fails (status = Failed)
+When you submit a change for processing, the response is a Change model. You can use the information in that change model in order to poll for the status of the change until it completes (status = Complete) or fails (status = Failed).
-Retrieves a list of RecordSet changes in a zone. All RecordSet changes are stored, including those coming from zone syncs. RecordSet changes come in max page sizes of 100 changes, paging must be done independently using startFrom and nextId parameters
+Retrieves a list of RecordSet changes in a zone. All RecordSet changes are stored, including those coming from zone syncs. RecordSet changes come in max page sizes of 100 changes, paging must be done independently using startFrom and nextId parameters.
#### HTTP REQUEST
@@ -19,7 +19,7 @@ Retrieves a list of RecordSet changes in a zone. All RecordSet changes are store
name | type | required? | description |
------------ | ------------- | ----------- | :---------- |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | int | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -37,8 +37,8 @@ name | type | description |
------------ | ------------- | :---------- |
zoneId | string | Id of zone used for request |
recordSetChanges | array of recordset changes | array of recordset changes sorted by created time in descending order |
-startFrom | *any* | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
-nextId | *any* | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
+startFrom | int | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | int | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
status | string | The status of the change (Pending, Complete, Failed) |
diff --git a/modules/docs/src/main/mdoc/api/list-recordsets-by-zone.md b/modules/docs/src/main/mdoc/api/list-recordsets-by-zone.md
index 329f26d55..23e480841 100644
--- a/modules/docs/src/main/mdoc/api/list-recordsets-by-zone.md
+++ b/modules/docs/src/main/mdoc/api/list-recordsets-by-zone.md
@@ -6,7 +6,7 @@ section: "api"
# List / Search RecordSets by Zone
-Retrieves a list of RecordSets from the zone
+Retrieves a list of RecordSets from the zone.
#### HTTP REQUEST
@@ -19,8 +19,8 @@ name | type | required? | description |
recordNameFilter | string | no | Characters that are part of the record name to search for. The wildcard character `*` is supported, for example `www*`. Omit the wildcard when searching for an exact record name. |
recordTypeFilter | Array of RecordType | no | An array of record types to filter for listing record sets. Refer to [recordset mode](recordset-model.html) for supported types. Invalid record types will be ignored. If left empty or no valid record types are provided, then all record types will be returned. |
nameSort | string | no | Name sort order for record sets returned by list record set response. Valid values are `ASC` (ascending; default) and `DESC` (descending).
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
-maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -36,8 +36,8 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
recordSets | Array of RecordSets | refer to [recordset model](recordset-model.html), the RecordSet data will also include the accessLevel the requesting user has based off acl rules and membership in Zone Admin Group |
-startFrom | *any* | startFrom sent in request, will not be returned if not provided |
-nextId | *any* | nextId, used as startFrom parameter of next page request, will not be returned if record sets are exhausted |
+startFrom | string | startFrom sent in request, will not be returned if not provided |
+nextId | string | nextId, used as startFrom parameter of next page request, will not be returned if record sets are exhausted |
maxItems | integer | maxItems sent in request, default is 100 |
recordNameFilter | string | name filter sent in request |
recordTypeFilter | Array of RecordType | record type filter sent in request |
diff --git a/modules/docs/src/main/mdoc/api/list-recordsets-global.md b/modules/docs/src/main/mdoc/api/list-recordsets-global.md
index 7ab6bb8a6..a368e0fd4 100644
--- a/modules/docs/src/main/mdoc/api/list-recordsets-global.md
+++ b/modules/docs/src/main/mdoc/api/list-recordsets-global.md
@@ -20,7 +20,7 @@ recordNameFilter | string | yes | Characters that are part of the re
recordTypeFilter | Array of RecordType | no | An array of record types to filter for listing record sets. Refer to [recordset mode](recordset-model.html) for supported types. Invalid record types will be ignored. If left empty or no valid record types are provided, then all record types will be returned. |
recordOwnerGroupFilter | string | no | Owner group ID for record set. |
nameSort | string | no | Name sort order for record sets returned by list record set response. Valid values are `ASC` (ascending; default) and `DESC` (descending). |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | integer | no | The number of items to return in the page. Valid values are 1 to 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -35,8 +35,8 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
recordSets | Array of RecordSets | refer to [recordset model](recordset-model.html) |
-startFrom | *any* | startFrom sent in request, will not be returned if not provided |
-nextId | *any* | nextId, used as startFrom parameter of next page request, will not be returned if record sets are exhausted |
+startFrom | string | startFrom sent in request, will not be returned if not provided |
+nextId | string | nextId, used as startFrom parameter of next page request, will not be returned if record sets are exhausted |
maxItems | integer | `maxItems` sent in request, default is 100 |
recordNameFilter | string | name filter sent in request |
recordTypeFilter | Array of RecordType | record type filter sent in request |
diff --git a/modules/docs/src/main/mdoc/api/list-zone-change-failures.md b/modules/docs/src/main/mdoc/api/list-zone-change-failures.md
new file mode 100644
index 000000000..20c510148
--- /dev/null
+++ b/modules/docs/src/main/mdoc/api/list-zone-change-failures.md
@@ -0,0 +1,121 @@
+---
+layout: docs
+title: "List Zone Change Failures"
+section: "api"
+---
+
+# List Zone Change Failures
+
+Retrieves a list of failed zone changes.
+
+#### HTTP REQUEST
+
+> GET metrics/health/zonechangesfailure?startFrom={response.nextId}&maxItems={1 - 100}
+
+#### HTTP REQUEST PARAMS
+
+name | type | required? | description |
+ ------------ | ------------- | ----------- | :---------- |
+startFrom | int | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
+
+#### HTTP RESPONSE TYPES
+
+Code | description |
+ ------------ | :---------- |
+200 | **Accepted** - The zone changes will be returned in the response body|
+401 | **Unauthorized** - The authentication information provided is invalid. Typically the request was not signed properly, or the access key and secret used to sign the request are incorrect |
+403 | **Forbidden** - The user does not have the access required to perform the action |
+404 | **Not Found** - Zone not found |
+
+#### HTTP RESPONSE ATTRIBUTES
+
+name | type | description |
+ ------------------ | ---------------------------- | :---------- |
+failedZoneChanges | array of zone changes | Array of failed zone changes sorted by created time in descending order. Refer to [Zone Change](#zone-change) |
+startFrom | int | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | int | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
+maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
+
+#### ZONE CHANGE ATTRIBUTES
+
+name | type | description |
+ ----------------- | ------------- | :---------- |
+zone | map | Refer to [zone model](zone-model.html) |
+status | string | The status of the change. Either Pending, Failed or Synced |
+changeType | string | The type of change. Either Create, Update, Delete, Sync or AutomatedSync |
+systemMessage | string | (optional) A message regarding the change. Will not be present if the string is empty |
+created | string | Millisecond timestamp that change was created
+userId | string | User Id of user who made the change |
+id | string | Id of the group change |
+
+#### EXAMPLE RESPONSE
+
+```json
+{
+ "failedZoneChanges": [
+ {
+ "zone": {
+ "name": "shared.",
+ "email": "email",
+ "status": "Active",
+ "created": "2022-05-20T16:47:28Z",
+ "updated": "2022-12-08T20:14:19Z",
+ "id": "e6efbae3-ae2d-466d-bfa4-207aa276a024",
+ "account": "system",
+ "shared": true,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "shared-zone-group",
+ "isTest": true
+ },
+ "userId": "6741d4df-81b7-40bd-9856-896c730c189b",
+ "changeType": "Sync",
+ "status": "Failed",
+ "created": "2022-12-08T20:14:19Z",
+ "id": "9ce7dc8b-a5ed-488d-82d4-8c7e7cf33285"
+ },
+ {
+ "zone": {
+ "name": "dummy.",
+ "email": "test@test.com",
+ "status": "Active",
+ "created": "2020-11-17T18:50:46Z",
+ "updated": "2022-10-17T10:13:46Z",
+ "id": "d7e433df-ad84-4fbe-9f52-3b2f3665412a",
+ "connection": {
+ "name": "dummy.",
+ "keyName": "vinyldns.",
+ "key": "OBF:1:uFOhH4AH8xEAABAAlertajQQHrQZB91yWQz5lyBf4O88js2S6aWNMtAq5MS5Otysb4Z7iiO9DoGY9A6BrDQ52b8SOQyj0QpzgPe0CuI/pLW1s/rulmlvgubHkIl7dsYAaRH7SrmZfNBe4BSn02zuv/ATyWEy",
+ "primaryServer": "96.115.238.13",
+ "algorithm": "HMAC-MD5"
+ },
+ "transferConnection": {
+ "name": "dummy.",
+ "keyName": "vinyldns.",
+ "key": "OBF:1:EonJvAJrMwQAABAAO+MPQq6fyNQcjnXUuV6YtvjeCGt8SEicWC6Ke9dLT1UmL4vAtlVg0nARl9rvhb1mxNndSf4ogx+/BvZx2AEvkTgCFxbsPMxJ/s6E/s6uaxa4sf/8+CpnR/1R0oYmfOaMSq04tgD+A+ym",
+ "primaryServer": "96.115.238.13",
+ "algorithm": "HMAC-MD5"
+ },
+ "account": "system",
+ "shared": false,
+ "acl": {
+ "rules": []
+ },
+ "adminGroupId": "9945e0c5-41dd-42e9-a053-3f4dacf006c3",
+ "latestSync": "2020-11-17T18:50:46Z",
+ "isTest": true
+ },
+ "userId": "c1f17f3e-59cc-491d-a1f3-ee3f50f38a09",
+ "changeType": "Sync",
+ "status": "Failed",
+ "created": "2022-10-17T10:13:46Z",
+ "id": "1cd5876f-b896-4823-86a6-286f21fa6a16"
+ }
+ ],
+ "nextId": 0,
+ "startFrom": 0,
+ "maxItems": 100
+}
+```
diff --git a/modules/docs/src/main/mdoc/api/list-zone-changes.md b/modules/docs/src/main/mdoc/api/list-zone-changes.md
index 131faaeca..39c5e6a82 100644
--- a/modules/docs/src/main/mdoc/api/list-zone-changes.md
+++ b/modules/docs/src/main/mdoc/api/list-zone-changes.md
@@ -6,7 +6,7 @@ section: "api"
# List Zone Changes
-Retrieves a list of zone changes to a zone. All zone changes are stored, including those coming from zone syncs. Zone changes come in max page sizes of 100 changes, paging must be done independently using startFrom and nextId parameters
+Retrieves a list of zone changes to a zone. All zone changes are stored, including those coming from zone syncs. Zone changes come in max page sizes of 100 changes, paging must be done independently using startFrom and nextId parameters.
#### HTTP REQUEST
@@ -16,7 +16,7 @@ Retrieves a list of zone changes to a zone. All zone changes are stored, includi
name | type | required? | description |
------------ | ------------- | ----------- | :---------- |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
#### HTTP RESPONSE TYPES
@@ -30,13 +30,25 @@ Code | description |
#### HTTP RESPONSE ATTRIBUTES
-name | type | description |
- ------------ | ------------- | :---------- |
-zoneId | string | Id of zone used for request |
-zoneChanges | array of zone changes | array of zone changes sorted by created time in descending order |
-startFrom | *any* | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
-nextId | *any* | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
-maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
+name | type | description |
+ ------------ | --------------------- | :---------- |
+zoneId | string | Id of zone used for request |
+zoneChanges | array of zone changes | Array of zone changes sorted by created time in descending order. Refer to [Zone Change](#zone-change) |
+startFrom | string | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | string | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present |
+maxItems | int | The maxItems parameter that was sent in on the HTTP request. This will be 100 if not sent |
+
+#### ZONE CHANGE ATTRIBUTES
+
+name | type | description |
+ ----------------- | ------------- | :---------- |
+zone | map | Refer to [zone model](zone-model.html) |
+status | string | The status of the change. Either Pending, Failed or Synced |
+changeType | string | The type of change. Either Create, Update, Delete, Sync or AutomatedSync |
+systemMessage | string | (optional) A message regarding the change. Will not be present if the string is empty |
+created | string | Millisecond timestamp that change was created
+userId | string | User Id of user who made the change |
+id | string | Id of the group change |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/list-zones.md b/modules/docs/src/main/mdoc/api/list-zones.md
index ed4c7e4c4..f29c7e671 100644
--- a/modules/docs/src/main/mdoc/api/list-zones.md
+++ b/modules/docs/src/main/mdoc/api/list-zones.md
@@ -10,16 +10,18 @@ Retrieves the list of zones a user has access to. The zone name is only sorted
#### HTTP REQUEST
-> GET /zones?nameFilter={yoursearchhere}&startFrom={response.nextId}&maxItems={1 - 100}&ignoreAccess={true|false}
+> GET /zones?nameFilter={yoursearchhere}&startFrom={response.nextId}&maxItems={1 - 100}&ignoreAccess={true | false}&searchByAdminGroup={true | false}&includeReverse={true | false}
#### HTTP REQUEST PARAMS
name | type | required? | description |
------------ | ------------- | ----------- | :---------- |
nameFilter | string | no | Characters that are part of the zone name to search for. The wildcard character `*` is supported, for example `www*`. Omit the wildcard character when searching for an exact zone name. |
-startFrom | *any* | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
+startFrom | string | no | In order to advance through pages of results, the startFrom is set to the `nextId` that is returned on the previous response. It is up to the client to maintain previous pages if the client wishes to advance forward and backward. If not specified, will return the first page of results |
maxItems | int | no | The number of items to return in the page. Valid values are 1 - 100. Defaults to 100 if not provided. |
-ignoreAccess | boolean | no | If false, returns only zones the requesting user owns or has ACL access to. If true, returns zones in the system, regardless of ownership. Defaults to false if not provided. |
+ignoreAccess | boolean | no | If false, returns only zones the requesting user owns or has ACL access to. If true, returns zones in the system, regardless of ownership. Defaults to false if not provided. |
+searchByAdminGroup | boolean | no | Used along with `nameFilter`. If false, returns a list of zones based on `nameFilter` value. If true, uses `nameFilter` value and filters the zone based on a group. Returns all the zones that is owned by a group given in the `nameFilter`. Defaults to false if not provided. |
+includeReverse | boolean | no | If false, returns only the forward zones. If true, returns both forward and reverse zones. |
#### HTTP RESPONSE TYPES
@@ -34,10 +36,12 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
zones | Array of [Zones](zone-model.html#zone-attributes) | An array of the zones found. The zones are sorted alphabetically by zone name. |
-startFrom | *any* | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
-nextId | *any* | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present.|
-maxItems | int | The maxItems parameter that was sent in the HTTP request. This will be 100 if not sent. |
-ignoreAccess | boolean | The ignoreAccess parameter that was sent in the HTTP request. This will be false if not sent. |
+startFrom | string | (optional) The startFrom parameter that was sent in on the HTTP request. Will not be present if the startFrom parameter was not sent |
+nextId | string | (optional) The identifier to be passed in as the *startFrom* parameter to retrieve the next page of results. If there are no results left, this field will not be present.|
+maxItems | int | The maxItems parameter that was sent in the HTTP request. This will be 100 if not sent. |
+ignoreAccess | boolean | The ignoreAccess parameter that was sent in the HTTP request. This will be false if not sent. |
+searchByAdminGroup | boolean | The searchByAdminGroup parameter that was sent in the HTTP request. This will be false if not sent. |
+includeReverse | boolean | The includeReverse parameter that was sent in the HTTP request. This will be false if not sent. |
#### EXAMPLE RESPONSE
diff --git a/modules/docs/src/main/mdoc/api/membership-model.md b/modules/docs/src/main/mdoc/api/membership-model.md
index 78a67498c..4c7fb0b8e 100644
--- a/modules/docs/src/main/mdoc/api/membership-model.md
+++ b/modules/docs/src/main/mdoc/api/membership-model.md
@@ -21,7 +21,7 @@ will be an admin of that zone, and can preform zone syncs, zone updates, zone de
of any Access Control Rules set on them.
While users in the admin group will have complete zone access, further users can be given limited membership through [Zone
-ACL Rules](zone-model.html#zone-acl-rule-attr)
+ACL Rules](zone-model.html#zone-acl-rule-attr).
#### GROUP ATTRIBUTES
diff --git a/modules/docs/src/main/mdoc/api/update-group.md b/modules/docs/src/main/mdoc/api/update-group.md
index 951e6d2fb..7befcc6ab 100644
--- a/modules/docs/src/main/mdoc/api/update-group.md
+++ b/modules/docs/src/main/mdoc/api/update-group.md
@@ -6,7 +6,7 @@ section: "api"
# Update Group
-Updates a Group in VinylDNS
+Updates a Group in VinylDNS.
#### HTTP REQUEST
diff --git a/modules/docs/src/main/mdoc/api/update-zone.md b/modules/docs/src/main/mdoc/api/update-zone.md
index ed3b92e51..77f500d67 100644
--- a/modules/docs/src/main/mdoc/api/update-zone.md
+++ b/modules/docs/src/main/mdoc/api/update-zone.md
@@ -14,7 +14,7 @@ Updates an existing zone that has already been connected to. Used to update the
#### HTTP REQUEST PARAMS
-**zone fields**, refer to [zone model](zone-model.html) |
+**Zone fields** - Refer to [zone model](zone-model.html).
#### EXAMPLE HTTP REQUEST
@@ -54,7 +54,7 @@ Code | description |
name | type | description |
------------ | ------------- | :---------- |
-zone | map | Zone sent with update request, refer to [zone model](zone-model.html) |
+zone | map | Zone sent with update request. Refer to [zone model](zone-model.html) |
userId | string | The user id that initiated the change |
changeType | string | Type of change requested (Create, Update, Sync, Delete); in this case Update |
created | string | The timestamp (UTC) the change was initiated |
diff --git a/modules/docs/src/main/mdoc/faq.md b/modules/docs/src/main/mdoc/faq.md
index 0a4def4c0..45e982fdd 100644
--- a/modules/docs/src/main/mdoc/faq.md
+++ b/modules/docs/src/main/mdoc/faq.md
@@ -18,6 +18,7 @@ position: 6
7. [When I try to connect to my zone, I am seeing "invalid name server" errors](#7)
8. [How do I get API credentials?](#8)
9. [How are requests authenticated to the VinylDNS API?](#9)
+10. [Why am I not able to view the Change History tab on a Group?](#10)
### 1. Can I create a zone in VinylDNS?
@@ -88,3 +89,7 @@ If you use any VinylDNS tools beyond the portal you will need to provide those t
### 9. How are requests authenticated to the VinylDNS API?
Refer to [API Authentication](api/auth-mechanism.html).
+
+### 10. Why am I not able to view the Change History tab on a Group?
+To view a group's change history, you should be a member or admin of that group. Only individuals who are part of the
+group can view the change history.
diff --git a/modules/docs/src/main/mdoc/operator/config-api.md b/modules/docs/src/main/mdoc/operator/config-api.md
index fa8b69ba3..c25243e51 100644
--- a/modules/docs/src/main/mdoc/operator/config-api.md
+++ b/modules/docs/src/main/mdoc/operator/config-api.md
@@ -15,12 +15,13 @@ section: "operator_menu"
- [Queue Configuration](#queue-configuration)
- [Database Configuration](#database-configuration)
- [Cryptography](#cryptography-settings)
+- [Zone Connections](#zone-connections)
- [Additional Configuration Settings](#additional-configuration-settings)
- [Full Example Config](#full-example-config)
There are a lot of configuration settings in VinylDNS. So much so that it may seem overwhelming to configure vinyldns to
your environment. This document describes the configuration settings, highlighting the settings you are _most likely to
-change_. All of the configuration settings are captured at the end.
+change_. All the configuration settings are captured at the end.
It is important to note that the `api` and `portal` have _different_ configuration. We will review the configuration for
each separately.
@@ -271,7 +272,7 @@ vinyldns {
}
```
-## Default Zone Connections
+## Zone Connections
VinylDNS has three ways of indicating zone connections:
@@ -291,6 +292,7 @@ VinylDNS also ties in testing network connectivity to the default zone connectio
checks. A value for the health check connection timeout in milliseconds can be specified using `health-check-timeout`; a
default value of 10000 will be used if not provided.
+### Global Zone Connections Configuration:
```yaml
vinyldns {
@@ -347,6 +349,93 @@ vinyldns {
]
```
+### Alternate Zone Connections Configuration:
+Below is an alternate way of setting zone connections configuration instead of using the [Global Zone Connections
+Configuration](#global-zone-connections-configuration)
+```yaml
+# configured backend providers
+backend {
+# Use "default" when dns backend legacy = true
+# otherwise, use the id of one of the connections in any of your backends
+default-backend-id = "default"
+
+# this is where we can save additional backends
+backend-providers = [
+ {
+ class-name = "vinyldns.api.backend.dns.DnsBackendProviderLoader"
+ settings = {
+ legacy = false
+ backends = [
+ {
+ id = "default"
+ zone-connection = {
+ name = "vinyldns."
+ key-name = "vinyldns."
+ key = "nzisn+4G2ldMn0q1CV3vsg=="
+ primary-server = "127.0.0.1:19001"
+ }
+ transfer-connection = {
+ name = "vinyldns."
+ key-name = "vinyldns."
+ key = "nzisn+4G2ldMn0q1CV3vsg=="
+ primary-server = "127.0.0.1:19001"
+ },
+ tsig-usage = "always"
+ },
+ {
+ id = "func-test-backend"
+ zone-connection = {
+ name = "vinyldns."
+ key-name = "vinyldns."
+ key = "nzisn+4G2ldMn0q1CV3vsg=="
+ primary-server = "127.0.0.1:19001"
+ }
+ transfer-connection = {
+ name = "vinyldns."
+ key-name = "vinyldns."
+ key = "nzisn+4G2ldMn0q1CV3vsg=="
+ primary-server = "127.0.0.1:19001"
+ },
+ tsig-usage = "always"
+ }
+ ]
+ }
+ }
+]
+}
+```
+
+Below is an example configuration of backend provider for AWS Route 53, in case we want to use AWS Route 53 as backend.
+```yaml
+backend {
+ default-backend-id = "r53"
+
+ backend-providers = [
+ {
+ class-name = "vinyldns.route53.backend.Route53BackendProviderLoader"
+ settings = {
+ backends = [
+ {
+ # AWS access key and secret key.
+ access-key = "your-access-key"
+ secret-key = "your-secret-key"
+
+ # Regional endpoint to make your requests (eg. 'us-west-2', 'us-east-1', etc.). This is the region where your queue is housed.
+ signing-region = "us-east-1"
+
+ # Endpoint to access r53
+ service-endpoint = "https://route53.amazonaws.com/"
+
+ id = "r53"
+ }
+ ]
+ }
+ }
+ ]
+ }
+```
+Make sure to add AWS name servers in [Approved Name Servers Config](#approved-name-servers).
+
## Additional Configuration Settings
### Approved Name Servers
@@ -388,6 +477,14 @@ Version of the application that is deployed. Currently, this is a configuration
**Note: You can get installation information including color, version, default key name, and processing-disabled by
hitting the _status_ endpoint GET /status**
+### Is Zone Sync Schedule Allowed
+
+Used while deploying. Should be set to `true` only on one api server/instance and `false` on every other api servers/instances.
+Thus automated sync will be done only once on a single server/instance instead of every api servers/instances.
+Set it to `true` while running locally or when we have only a single api server/instance.
+
+`is-zone-sync-schedule-allowed = true`
+
### HTTP Host and Port
To specify what host and port to bind to when starting up the API server, default is 9000.
@@ -475,6 +572,23 @@ sns {
}
}
```
+### Email Domain Configuration
+ This configuration setting determines the valid domains which are
+ allowed in the email fields.
+
+ The `email-domains` field accepts a list of valid email domains. Wildcard matching
+ is available; for example, `*dummy.com` means it will allow any
+ subdomain within dummy.com like test.dummy.com. If the `email-domains` field is
+ left empty then it will accept any domain name.
+
+The `number-of-dots` field controls the number of dots allowed after the `@` symbol in
+an email. If this config value is left out, it will default to two.
+```yaml
+valid-email-config {
+ email-domains = ["test.com","*dummy.com"]
+ number-of-dots= 2
+}
+ ```
### Batch Manual Review Enabled
@@ -752,6 +866,12 @@ dotted-hosts = {
}
}
+ # Valid Email Domains
+ valid-email-config {
+ email-domains = ["test.com","*dummy.com"]
+ number-of-dots= 2
+ }
+
sns {
# Path to notifier provider implementation
class-name = "vinyldns.api.notifier.sns.SnsNotifierProvider"
@@ -870,7 +990,10 @@ dotted-hosts = {
# The time period within which the TCP binding process must be completed.
# Set to `infinite` to disable.
bind-timeout = 5s
-
+ # A default request timeout is applied globally to all routes and can be configured using the
+ # akka.http.server.request-timeout setting (which defaults to 20 seconds).
+ # request-timeout = 60s
+
# Show verbose error messages back to the client
verbose-error-messages = on
}
diff --git a/modules/docs/src/main/mdoc/operator/config-portal.md b/modules/docs/src/main/mdoc/operator/config-portal.md
index 0d31f9a1e..a7ffb968f 100644
--- a/modules/docs/src/main/mdoc/operator/config-portal.md
+++ b/modules/docs/src/main/mdoc/operator/config-portal.md
@@ -198,6 +198,19 @@ The play secret must be set to a secret value, and should be an environment vari
play.http.secret.key = "vinyldnsportal-change-this-for-production"
```
+### Play Allowed Hosts Filter
+Play provides a filter that lets you configure which hosts can access your application. The filter introduces a
+whitelist of allowed hosts and sends a 400 (Bad Request) response to all requests with a host that do not match
+the whitelist.
+
+```yaml
+# See https://www.playframework.com/documentation/2.8.x/AllowedHostsFilter for more details.
+# Note: allowed = ["."] matches all hosts hence would not be recommended in a production environment.
+play.filters.hosts {
+ allowed = ["."]
+}
+```
+
### Test Login
The test login should not be used for production environments. It is useful to tinker with VinylDNS. If this
setting is true, then you can login with `testuser` and `testpassword`. Logging in using the `testuser` will _not_
@@ -210,6 +223,11 @@ The HTTP Port that the Portal server will bind to
`http.port=9001`
+### Portal URL
+Necessary to preview the metadata while sharing the portal link to social media
+
+`portal.vinyldns.url = "http://localhost:9001"`
+
### Shared Zones Display / Record Owner Selection
Necessary to enable shared zones submission and record ownership
@@ -244,10 +262,17 @@ Allows users to schedule changes to be run sometime in the future
# See https://www.playframework.com/documentation/latest/ApplicationSecret for more details.
play.http.secret.key = "vinyldnsportal-change-this-for-production"
+# See https://www.playframework.com/documentation/2.8.x/AllowedHostsFilter for more details.
+# Note: allowed = ["."] matches all hosts hence would not be recommended in a production environment.
+play.filters.hosts {
+ allowed = ["."]
+}
+
# The application languages
# ~~~~~
play.i18n.langs = [ "en" ]
portal.vinyldns.backend.url = "http://vinyldns-api:9000"
+portal.vinyldns.url = "http://localhost:9001"
portal.test_login = false
# configuration for the users and groups store
diff --git a/modules/docs/src/main/mdoc/operator/setup-ldap.md b/modules/docs/src/main/mdoc/operator/setup-ldap.md
index 1ccf61116..c2b4a8606 100644
--- a/modules/docs/src/main/mdoc/operator/setup-ldap.md
+++ b/modules/docs/src/main/mdoc/operator/setup-ldap.md
@@ -17,7 +17,7 @@ can read data from the Directory. Once you have that information, proceed to th
**Considerations**
You _should_ communicate to your Directory over LDAP using TLS. To do so, the SSL certs should be installed
on the portal servers, or provided via a java trust store (key store). The portal provides an option to specific
-a java key store when it starts up.
+a java key store when it starts up. For more information: [Using Java Key Store In VinylDNS](https://github.com/vinyldns/vinyldns/tree/master/modules/portal#building-locally)
## Configuring LDAP
Before you can configure LDAP, make note of the host, username, and password that you will be using.
diff --git a/modules/docs/src/main/mdoc/portal/zones.md b/modules/docs/src/main/mdoc/portal/zones.md
index 613c5fb36..698c9651a 100644
--- a/modules/docs/src/main/mdoc/portal/zones.md
+++ b/modules/docs/src/main/mdoc/portal/zones.md
@@ -24,3 +24,7 @@ Those records are accessible via the [DNS Changes](./dns-changes.html) area of t
[{:.screenshot}](../img/portal/zones-my-zones.png)
[{:.screenshot}](../img/portal/zones-all-zones.png)
+
+[{:.screenshot}](../img/portal/zones-my-deleted-zones.png)
+
+[{:.screenshot}](../img/portal/zones-all-deleted-zones.png)
\ No newline at end of file
diff --git a/modules/docs/src/main/resources/microsite/data/menu.yml b/modules/docs/src/main/resources/microsite/data/menu.yml
index 5fcbd93b8..84ad40676 100644
--- a/modules/docs/src/main/resources/microsite/data/menu.yml
+++ b/modules/docs/src/main/resources/microsite/data/menu.yml
@@ -135,6 +135,12 @@ options:
- title: List Zone Changes
url: api/list-zone-changes.html
menu_section: zoneapireference
+ - title: List Zone Change Failures
+ url: api/list-zone-change-failures.html
+ menu_section: zoneapireference
+ - title: Abandoned Zones
+ url: api/list-deleted-zones.html
+ menu_section: zoneapireference
- title: RecordSet
url: api/recordset-model.html
@@ -168,6 +174,15 @@ options:
- title: Global List / Search RecordSets
url: api/list-recordsets-global.html
menu_sectioon: recordsetapireference
+ - title: Get RecordSet Count
+ url: api/get-recordset-count.html
+ menu_section: recordsetapireference
+ - title: List RecordSet Change Failures
+ url: api/list-recordset-change-failures.html
+ menu_section: recordsetapireference
+ - title: Get RecordSet Change History
+ url: api/get-recordset-change-history.html
+ menu_section: recordsetapireference
- title: Batch Change
url: api/batchchange-model.html
@@ -234,3 +249,9 @@ options:
- title: List Group Activity
url: api/list-group-activity.html
menu_section: membershipapireference
+ - title: Get Group Change
+ url: api/get-group-change.html
+ menu_section: membershipapireference
+ - title: Get Valid Email Domains
+ url: api/get-valid-email-domains.html
+ menu_section: membershipapireference
diff --git a/modules/docs/src/main/resources/microsite/img/portal/zones-all-deleted-zones.png b/modules/docs/src/main/resources/microsite/img/portal/zones-all-deleted-zones.png
new file mode 100644
index 000000000..ce7861003
Binary files /dev/null and b/modules/docs/src/main/resources/microsite/img/portal/zones-all-deleted-zones.png differ
diff --git a/modules/docs/src/main/resources/microsite/img/portal/zones-my-deleted-zones.png b/modules/docs/src/main/resources/microsite/img/portal/zones-my-deleted-zones.png
new file mode 100644
index 000000000..02c201188
Binary files /dev/null and b/modules/docs/src/main/resources/microsite/img/portal/zones-my-deleted-zones.png differ
diff --git a/modules/docs/src/main/resources/microsite/static/dns-changes-csv-sample.csv b/modules/docs/src/main/resources/microsite/static/dns-changes-csv-sample.csv
index cb24f5955..04389a6a8 100644
--- a/modules/docs/src/main/resources/microsite/static/dns-changes-csv-sample.csv
+++ b/modules/docs/src/main/resources/microsite/static/dns-changes-csv-sample.csv
@@ -6,6 +6,11 @@ Add,AAAA,test3.example.com.,,fd69:27cc:fe91::60
Add,CNAME,test4.example.com.,,test0.example.com.
Add,PTR,192.0.2.193,200,test.example.com.
Add,TXT,test5.example.com,7200,example text
+Add,MX,test16.ok.,7200,1 text.com
+Add,NS,test17.ok.,7200,172.17.42.1
+Add,NAPTR,test18.ok.,7200,10 20 S SIPS+D2T _si._tc.ac-ot1.wdv.test.net.
+Add,NAPTR,test20.ok.,7200,10 20 U SIPS+D2T !^.*$!sip:jd@corpxyz.com! _si._tc.ac-ot1.wdv.test.net.
+Add,SRV,test19.ok.,7200,1 2 3 dummy.com
DeleteRecordSet,A+PTR,test5.example.com.,,1.1.1.1
DeleteRecordSet,A,test6.example.com.,,
DeleteRecordSet,AAAA+PTR,test7.example.com.,,fd69:27cc:fe91::60
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlBatchChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlBatchChangeRepositoryIntegrationSpec.scala
index 6ad03ab21..0426ab2a5 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlBatchChangeRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlBatchChangeRepositoryIntegrationSpec.scala
@@ -47,10 +47,10 @@ class MySqlBatchChangeRepositoryIntegrationSpec
object TestData {
def generateSingleAddChange(
- recordType: RecordType,
- recordData: RecordData,
- status: SingleChangeStatus = Pending,
- errors: List[SingleChangeError] = List.empty
+ recordType: RecordType,
+ recordData: RecordData,
+ status: SingleChangeStatus = Pending,
+ errors: List[SingleChangeError] = List.empty
): SingleAddChange =
SingleAddChange(
Some(okZone.id),
@@ -116,7 +116,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
val pendingBatchChange: BatchChange = randomBatchChange().copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS))
val completeBatchChange: BatchChange = randomBatchChangeWithList(
- randomBatchChange().changes.map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
+ randomBatchChange().changes.map(_.complete("recordChangeId", "recordSetId"))
).copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(1000))
val failedBatchChange: BatchChange =
@@ -124,7 +124,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
.copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(100000))
val partialFailureBatchChange: BatchChange = randomBatchChangeWithList(
- randomBatchChange().changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
+ randomBatchChange().changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
++ randomBatchChange().changes.drop(2).map(_.withFailureMessage("failed"))
).copy(createdTimestamp = Instant.now.truncatedTo(ChronoUnit.MILLIS).plusMillis(1000000))
@@ -141,7 +141,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
val change_two: BatchChange =
completeBatchChange.copy(createdTimestamp = timeBase.plusMillis(1000), ownerGroupId = None)
val otherUserBatchChange: BatchChange =
- randomBatchChange().copy(userId = "Other", createdTimestamp = timeBase.plusMillis(50000))
+ randomBatchChange().copy(userId = "Other", userName = "Other", createdTimestamp = timeBase.plusMillis(50000))
val change_three: BatchChange = failedBatchChange.copy(createdTimestamp = timeBase.plusMillis(100000))
val change_four: BatchChange =
partialFailureBatchChange.copy(createdTimestamp = timeBase.plusMillis(1000000))
@@ -150,6 +150,8 @@ class MySqlBatchChangeRepositoryIntegrationSpec
createdTimestamp = timeBase.plusMillis(10000000),
approvalStatus = BatchChangeApprovalStatus.ManuallyRejected
)
+ val change_six: BatchChange =
+ completeBatchChange.copy(createdTimestamp = timeBase.plusMillis(2000), ownerGroupId = None)
}
import TestData._
@@ -411,7 +413,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"update single changes" in {
val batchChange = randomBatchChange()
- val completed = batchChange.changes.map(_.complete(Some("Complete"),"aaa", "bbb"))
+ val completed = batchChange.changes.map(_.complete("aaa", "bbb"))
val f =
for {
_ <- repo.save(batchChange)
@@ -430,7 +432,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"update some changes in a batch" in {
val batchChange = randomBatchChange()
- val completed = batchChange.changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
+ val completed = batchChange.changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
val incomplete = batchChange.changes.drop(2)
val f =
for {
@@ -444,7 +446,7 @@ class MySqlBatchChangeRepositoryIntegrationSpec
"return the batch when updating single changes" in {
val batchChange = randomBatchChange()
- val completed = batchChange.changes.take(2).map(_.complete(Some("Complete"),"recordChangeId", "recordSetId"))
+ val completed = batchChange.changes.take(2).map(_.complete("recordChangeId", "recordSetId"))
val f =
for {
_ <- repo.save(batchChange)
@@ -512,6 +514,213 @@ class MySqlBatchChangeRepositoryIntegrationSpec
areSame(f.unsafeRunSync(), expectedChanges)
}
+ "get batch change summaries by user name" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(None, userName = Some(pendingBatchChange.userName))
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_four),
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two),
+ BatchChangeSummary(change_one)
+ )
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get batch change summaries by user name with maxItems" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(None, userName = Some(pendingBatchChange.userName), maxItems = 3)
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_four),
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two)
+ ),
+ None,
+ Some(3),
+ 3
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get batch change summaries by user name with explicit startFrom" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(
+ None,
+ userName = Some(pendingBatchChange.userName),
+ startFrom = Some(1),
+ maxItems = 3
+ )
+ } yield retrieved
+
+ // sorted from most recent descending. startFrom uses zero-based indexing.
+ // Expect to get only the second batch change, change_3.
+ // No nextId because the maxItems (3) equals the number of batch changes the user has after the offset (3)
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two),
+ BatchChangeSummary(change_one)
+ ),
+ Some(1),
+ None,
+ 3
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get batch change summaries by user name with explicit startFrom and maxItems" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(
+ None,
+ userName = Some(pendingBatchChange.userName),
+ startFrom = Some(1),
+ maxItems = 1
+ )
+ } yield retrieved
+
+ // sorted from most recent descending. startFrom uses zero-based indexing.
+ // Expect to get only the second batch change, change_3.
+ // Expect the ID of the next batch change to be 2.
+ val expectedChanges =
+ BatchChangeSummaryList(List(BatchChangeSummary(change_three)), Some(1), Some(2), 1)
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get second page of batch change summaries by user name" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved1 <- repo.getBatchChangeSummaries(None, userName = Some(pendingBatchChange.userName), maxItems = 1)
+ retrieved2 <- repo.getBatchChangeSummaries(
+ None,
+ userName = Some(pendingBatchChange.userName),
+ startFrom = retrieved1.nextId
+ )
+ } yield (retrieved1, retrieved2)
+
+ val expectedChanges =
+ BatchChangeSummaryList(List(BatchChangeSummary(change_four)), None, Some(1), 1)
+
+ val secondPageExpectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two),
+ BatchChangeSummary(change_one)
+ ),
+ Some(1),
+ None
+ )
+ val retrieved = f.unsafeRunSync()
+ areSame(retrieved._1, expectedChanges)
+ areSame(retrieved._2, secondPageExpectedChanges)
+ }
+
+ "get batch change summaries by user name and approval status" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(
+ None,
+ Some(pendingBatchChange.userName),
+ approvalStatus = Some(BatchChangeApprovalStatus.AutoApproved)
+ )
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_four),
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two)
+ )
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get batch change summaries by user ID, user name and approval status" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(
+ Some(pendingBatchChange.userId),
+ Some(pendingBatchChange.userName),
+ approvalStatus = Some(BatchChangeApprovalStatus.AutoApproved)
+ )
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_four),
+ BatchChangeSummary(change_three),
+ BatchChangeSummary(change_two)
+ )
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "return empty list if a batch change summary is not found by user name" in {
+ val batchChangeSummaries = repo.getBatchChangeSummaries(None, userName = Some("doesnotexist")).unsafeRunSync()
+ batchChangeSummaries.batchChanges shouldBe empty
+ }
+
"get batch change summaries by user ID" in {
val f =
for {
@@ -687,6 +896,59 @@ class MySqlBatchChangeRepositoryIntegrationSpec
batchChangeSummaries.batchChanges shouldBe empty
}
+ "get batch change summaries by batch change status" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(None, batchStatus = Some(BatchChangeStatus.PartialFailure))
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_four)
+ )
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "get batch change summaries by user ID, approval status and batch change status" in {
+ val f =
+ for {
+ _ <- repo.save(change_one)
+ _ <- repo.save(change_two)
+ _ <- repo.save(change_three)
+ _ <- repo.save(change_four)
+ _ <- repo.save(otherUserBatchChange)
+
+ retrieved <- repo.getBatchChangeSummaries(
+ Some(pendingBatchChange.userId),
+ approvalStatus = Some(BatchChangeApprovalStatus.AutoApproved),
+ batchStatus = Some(BatchChangeStatus.Complete)
+ )
+ } yield retrieved
+
+ // from most recent descending
+ val expectedChanges = BatchChangeSummaryList(
+ List(
+ BatchChangeSummary(change_two)
+ )
+ )
+
+ areSame(f.unsafeRunSync(), expectedChanges)
+ }
+
+ "return empty list if a batch change summary is not found for a batch change status" in {
+ val batchChangeSummaries = repo.getBatchChangeSummaries(None, batchStatus = Some(BatchChangeStatus.Scheduled)).unsafeRunSync()
+ batchChangeSummaries.batchChanges shouldBe empty
+ }
+
"properly status check (pending)" in {
val chg = randomBatchChange(
List(
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupChangeRepositoryIntegrationSpec.scala
index 895d12203..15593f6bf 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupChangeRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupChangeRepositoryIntegrationSpec.scala
@@ -102,7 +102,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
}
"MySqlGroupChangeRepository.getGroupChanges" should {
- "don't return lastEvaluatedTimeStamp if page size < maxItems" in {
+ "don't return nextId if page size < maxItems" in {
val groupId = "group-id-1"
val changes = generateGroupChanges(groupId, 50)
changes.map(saveGroupChangeData(repo, _).unsafeRunSync())
@@ -113,7 +113,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
val listResponse = repo.getGroupChanges(groupId, None, 100).unsafeRunSync()
listResponse.changes shouldBe expectedChanges
- listResponse.lastEvaluatedTimeStamp shouldBe None
+ listResponse.nextId shouldBe None
}
"get group changes properly using a maxItems of 1" in {
@@ -130,9 +130,7 @@ class MySqlGroupChangeRepositoryIntegrationSpec
val listResponse =
repo.getGroupChanges(groupId, startFrom = None, maxItems = 1).unsafeRunSync()
listResponse.changes shouldBe expectedChanges
- listResponse.lastEvaluatedTimeStamp shouldBe Some(
- expectedChanges.head.created.toEpochMilli.toString
- )
+ listResponse.nextId shouldBe Some(1)
}
"page group changes using a startFrom and maxItems" in {
@@ -145,33 +143,33 @@ class MySqlGroupChangeRepositoryIntegrationSpec
.reverse
val expectedPageOne = Seq(changesSorted(0))
- val expectedPageOneNext = Some(changesSorted(0).created.toEpochMilli.toString)
+ val expectedPageOneNext = Some(1)
val expectedPageTwo = Seq(changesSorted(1))
- val expectedPageTwoNext = Some(changesSorted(1).created.toEpochMilli.toString)
+ val expectedPageTwoNext = Some(2)
val expectedPageThree = Seq(changesSorted(2))
- val expectedPageThreeNext = Some(changesSorted(2).created.toEpochMilli.toString)
+ val expectedPageThreeNext = Some(3)
// get first page
val pageOne =
repo.getGroupChanges(groupId, startFrom = None, maxItems = 1).unsafeRunSync()
pageOne.changes shouldBe expectedPageOne
- pageOne.lastEvaluatedTimeStamp shouldBe expectedPageOneNext
+ pageOne.nextId shouldBe expectedPageOneNext
// get second page
val pageTwo =
repo
- .getGroupChanges(groupId, startFrom = pageOne.lastEvaluatedTimeStamp, maxItems = 1)
+ .getGroupChanges(groupId, startFrom = pageOne.nextId, maxItems = 1)
.unsafeRunSync()
pageTwo.changes shouldBe expectedPageTwo
- pageTwo.lastEvaluatedTimeStamp shouldBe expectedPageTwoNext
+ pageTwo.nextId shouldBe expectedPageTwoNext
// get final page
val pageThree =
repo
- .getGroupChanges(groupId, startFrom = pageTwo.lastEvaluatedTimeStamp, maxItems = 1)
+ .getGroupChanges(groupId, startFrom = pageTwo.nextId, maxItems = 1)
.unsafeRunSync()
pageThree.changes shouldBe expectedPageThree
- pageThree.lastEvaluatedTimeStamp shouldBe expectedPageThreeNext
+ pageThree.nextId shouldBe expectedPageThreeNext
}
}
}
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala
index d600318c3..c372bbca8 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlGroupRepositoryIntegrationSpec.scala
@@ -50,7 +50,7 @@ class MySqlGroupRepositoryIntegrationSpec
override protected def beforeAll(): Unit = {
DB.localTx { s =>
- s.executeUpdate("DELETE FROM groups")
+ s.executeUpdate("DELETE FROM `groups`")
}
for (group <- groups) {
@@ -60,7 +60,7 @@ class MySqlGroupRepositoryIntegrationSpec
override protected def afterAll(): Unit = {
DB.localTx { s =>
- s.executeUpdate("DELETE FROM groups")
+ s.executeUpdate("DELETE FROM `groups`")
}
super.afterAll()
}
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordChangeRepositoryIntegrationSpec.scala
index fbada6769..c6636c7d2 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordChangeRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordChangeRepositoryIntegrationSpec.scala
@@ -17,16 +17,17 @@
package vinyldns.mysql.repository
import java.util.UUID
-
import cats.scalatest.EitherMatchers
import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scalikejdbc._
-import vinyldns.core.domain.record.{ChangeSet, RecordChangeRepository, RecordSetChange, RecordSetChangeType}
+import vinyldns.core.domain.record.{ChangeSet, RecordChangeRepository, RecordSetChange, RecordSetChangeStatus, RecordSetChangeType, RecordType}
import vinyldns.core.domain.zone.Zone
import vinyldns.mysql.TestMySqlInstance
import vinyldns.mysql.TransactionProvider
+import java.time.Instant
+
class MySqlRecordChangeRepositoryIntegrationSpec
extends AnyWordSpec
with Matchers
@@ -61,6 +62,24 @@ class MySqlRecordChangeRepositoryIntegrationSpec
newRecordSets.map(makeTestAddChange(_, zone)).toList
}
+ def generateSameInserts(zone: Zone, count: Int): List[RecordSetChange] = {
+ val newRecordSets =
+ for {
+ i <- 1 to count
+ } yield aaaa.copy(zoneId = zone.id, name = s"apply-test", id = UUID.randomUUID().toString, created = Instant.now.plusSeconds(i))
+
+ newRecordSets.map(makeTestAddChange(_, zone)).toList
+ }
+
+ def generateFailedInserts(zone: Zone, count: Int): List[RecordSetChange] = {
+ val newRecordSets =
+ for {
+ i <- 1 to count
+ } yield aaaa.copy(zoneId = zone.id, name = s"$i-apply-test", id = UUID.randomUUID().toString)
+
+ newRecordSets.map(makeTestAddChange(_, zone).copy(status= RecordSetChangeStatus.Failed)).toList
+ }
+
"saving record changes" should {
"save a batch of inserts" in {
val inserts = generateInserts(okZone, 1000)
@@ -94,7 +113,7 @@ class MySqlRecordChangeRepositoryIntegrationSpec
repo.save(db, ChangeSet(inserts))
}
saveRecChange.attempt.unsafeRunSync() shouldBe right
- val result = repo.listRecordSetChanges(okZone.id, None, 5).unsafeRunSync()
+ val result = repo.listRecordSetChanges(Some(okZone.id), None, 5).unsafeRunSync()
result.nextId shouldBe defined
result.maxItems shouldBe 5
(result.items should have).length(5)
@@ -113,20 +132,105 @@ class MySqlRecordChangeRepositoryIntegrationSpec
repo.save(db, ChangeSet(timeSpaced))
}
saveRecChange.attempt.unsafeRunSync() shouldBe right
- val page1 = repo.listRecordSetChanges(okZone.id, None, 2).unsafeRunSync()
- page1.nextId shouldBe Some(expectedOrder(1).created.toEpochMilli.toString)
+ val page1 = repo.listRecordSetChanges(Some(okZone.id), None, 2).unsafeRunSync()
+ page1.nextId shouldBe Some(2)
page1.maxItems shouldBe 2
(page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
- val page2 = repo.listRecordSetChanges(okZone.id, page1.nextId, 2).unsafeRunSync()
- page2.nextId shouldBe Some(expectedOrder(3).created.toEpochMilli.toString)
+ val page2 = repo.listRecordSetChanges(Some(okZone.id), page1.nextId, 2).unsafeRunSync()
+ page2.nextId shouldBe Some(4)
page2.maxItems shouldBe 2
(page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(2, 4))
- val page3 = repo.listRecordSetChanges(okZone.id, page2.nextId, 2).unsafeRunSync()
+ val page3 = repo.listRecordSetChanges(Some(okZone.id), page2.nextId, 2).unsafeRunSync()
page3.nextId shouldBe None
page3.maxItems shouldBe 2
page3.items should contain theSameElementsAs expectedOrder.slice(4, 5)
}
+ "list a particular recordset's changes by fqdn and record type" in {
+ val inserts = generateInserts(okZone, 10)
+ val saveRecChange = executeWithinTransaction { db: DB =>
+ repo.save(db, ChangeSet(inserts))
+ }
+ saveRecChange.attempt.unsafeRunSync() shouldBe right
+ val result = repo.listRecordSetChanges(None, None, 5, Some("1-apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
+ result.nextId shouldBe None
+ result.maxItems shouldBe 5
+ (result.items should have).length(1)
+ }
+ "page through a particular recordset's changes by fqdn and record type" in {
+ val inserts = generateSameInserts(okZone, 8)
+ val saveRecChange = executeWithinTransaction { db: DB =>
+ repo.save(db, ChangeSet(inserts))
+ }
+ saveRecChange.attempt.unsafeRunSync() shouldBe right
+ val page1 = repo.listRecordSetChanges(None, None, 5, Some("apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
+ page1.nextId shouldBe defined
+ page1.maxItems shouldBe 5
+ (page1.items should have).length(5)
+
+ val page2 = repo.listRecordSetChanges(None, page1.nextId, 5, Some("apply-test.ok.zone.recordsets."), Some(RecordType.AAAA)).unsafeRunSync()
+ page2.nextId shouldBe None
+ page2.maxItems shouldBe 5
+ (page2.items should have).length(3)
+ }
+ }
+
+ "list failed record changes" should {
+ "return records for failed record changes" in {
+ val inserts = generateFailedInserts(okZone, 10)
+ val saveRecChange = executeWithinTransaction { db: DB =>
+ repo.save(db, ChangeSet(inserts))
+ }
+ saveRecChange.attempt.unsafeRunSync() shouldBe right
+ val result = repo.listFailedRecordSetChanges(Some(okZone.id),10, 0).unsafeRunSync()
+ (result.items should have).length(10)
+ result.maxItems shouldBe 10
+ result.items should contain theSameElementsAs(inserts)
+ }
+
+ "return empty for success record changes" in {
+ val inserts = generateInserts(okZone, 10)
+ val saveRecChange = executeWithinTransaction { db: DB =>
+ repo.save(db, ChangeSet(inserts))
+ }
+ saveRecChange.attempt.unsafeRunSync() shouldBe right
+ val result = repo.listFailedRecordSetChanges(Some(okZone.id),5, 0).unsafeRunSync()
+ (result.items should have).length(0)
+ result.items shouldBe List()
+ result.nextId shouldBe 0
+ result.maxItems shouldBe 5
+ }
+ "page through failed record changes" in {
+ // sort by created desc, so adding additional seconds makes it more current, the last
+ val timeSpaced =
+ generateFailedInserts(okZone, 5).zipWithIndex.map {
+ case (c, i) => c.copy(created = c.created.plusSeconds(i))
+ }
+
+ // expect to be sorted by created descending so reverse that
+ val expectedOrder = timeSpaced.sortBy(_.created.toEpochMilli).reverse
+
+ val saveRecChange = executeWithinTransaction { db: DB =>
+ repo.save(db, ChangeSet(timeSpaced))
+ }
+ saveRecChange.attempt.unsafeRunSync() shouldBe right
+ val page1 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 0).unsafeRunSync()
+ page1.nextId shouldBe 3
+ page1.maxItems shouldBe 2
+ (page1.items should contain).theSameElementsInOrderAs(expectedOrder.take(2))
+
+ val page2 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, page1.nextId).unsafeRunSync()
+ page2.nextId shouldBe 6
+ page2.maxItems shouldBe 2
+ (page2.items should contain).theSameElementsInOrderAs(expectedOrder.slice(3, 5))
+
+ val page3 = repo.listFailedRecordSetChanges(Some(okZone.id), 2, 4).unsafeRunSync()
+ page3.nextId shouldBe 0
+ page3.maxItems shouldBe 2
+ page3.items should contain theSameElementsAs expectedOrder.slice(4, 5)
+ }
}
}
+
+
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordSetRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordSetRepositoryIntegrationSpec.scala
index cfd356972..1795925d0 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordSetRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlRecordSetRepositoryIntegrationSpec.scala
@@ -419,7 +419,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
"return all record sets in a zone when optional params are not set" in {
val existing = insert(okZone, 10).map(_.recordSet)
val found = repo
- .listRecordSets(Some(okZone.id), None, None, None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, None, None, None, None, NameSort.ASC, RecordTypeSort.ASC)
.unsafeRunSync()
found.recordSets should contain theSameElementsAs existing.map(
r => recordSetWithFQDN(r, okZone)
@@ -430,7 +430,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
val existing = insert(okZone, 5).map(_.recordSet).sortBy(_.name)
val startFrom = Some(PagingKey.toNextId(existing(2), true))
val found = repo
- .listRecordSets(Some(okZone.id), startFrom, None, None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), startFrom, None, None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(found.recordSets should contain).theSameElementsInOrderAs(
@@ -444,7 +444,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
val existing = insert(okZone, 5).map(_.recordSet).sortBy(_.name)
val startFrom = Some(PagingKey.toNextId(existing(1), true))
val found = repo
- .listRecordSets(Some(okZone.id), startFrom, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), startFrom, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(found.recordSets should contain).theSameElementsInOrderAs(
@@ -475,7 +475,8 @@ class MySqlRecordSetRepositoryIntegrationSpec
Some("*z*"),
None,
None,
- NameSort.ASC
+ NameSort.ASC,
+ RecordTypeSort.NONE
)
.unsafeRunSync()
(found.recordSets.map(_.name) should contain).theSameElementsInOrderAs(expectedNames)
@@ -493,7 +494,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
insert(changes)
val found = repo
- .listRecordSets(Some(okZone.id), None, Some(3), Some("aa*"), None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, Some(3), Some("aa*"), None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(found.recordSets.map(_.name) should contain).theSameElementsInOrderAs(expectedNames)
}
@@ -510,7 +511,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
insert(changes)
val found = repo
- .listRecordSets(Some(okZone.id), None, Some(3), Some("*b"), None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, Some(3), Some("*b"), None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(found.recordSets.map(_.name) should contain).theSameElementsInOrderAs(expectedNames)
}
@@ -528,14 +529,14 @@ class MySqlRecordSetRepositoryIntegrationSpec
insert(changes)
val found = repo
- .listRecordSets(Some(okZone.id), None, Some(3), Some("aaa"), None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, Some(3), Some("aaa"), None, None, NameSort.ASC, RecordTypeSort.ASC)
.unsafeRunSync()
(found.recordSets.map(_.name) should contain).theSameElementsInOrderAs(expectedNames)
}
"return select types of recordsets in a zone" in {
insert(okZone, 10).map(_.recordSet)
val found = repo
- .listRecordSets(Some(okZone.id), None, None, None, Some(Set(CNAME)), None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, None, None, Some(Set(CNAME)), None, NameSort.ASC,RecordTypeSort.ASC)
.unsafeRunSync()
found.recordSets shouldBe List()
found.recordTypeFilter shouldBe Some(Set(CNAME))
@@ -543,18 +544,30 @@ class MySqlRecordSetRepositoryIntegrationSpec
"return all recordsets in a zone in descending order" in {
val existing = insert(okZone, 10).map(_.recordSet)
val found = repo
- .listRecordSets(Some(okZone.id), None, None, None, None, None, NameSort.DESC)
+ .listRecordSets(Some(okZone.id), None, None, None, None, None, NameSort.DESC, RecordTypeSort.NONE)
.unsafeRunSync()
found.recordSets should contain theSameElementsAs existing.map(
r => recordSetWithFQDN(r, okZone)
)
found.nameSort shouldBe NameSort.DESC
}
+
+ "return all recordsets record type in a zone in descending order" in {
+ val existing = insert(okZone, 10).map(_.recordSet)
+ val found = repo
+ .listRecordSets(Some(okZone.id), None, None, None, None, None, NameSort.ASC, RecordTypeSort.DESC)
+ .unsafeRunSync()
+ found.recordSets should contain theSameElementsAs existing.map(
+ r => recordSetWithFQDN(r, okZone)
+ )
+ found.recordTypeSort shouldBe RecordTypeSort.DESC
+ }
+
"pages through the list properly" in {
// load 5 records, pages of 2, last page should have 1 result and no next id
val existing = insert(okZone, 5).map(_.recordSet).sortBy(_.name)
val page1 = repo
- .listRecordSets(Some(okZone.id), None, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(page1.recordSets should contain).theSameElementsInOrderAs(
existing
@@ -564,7 +577,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
page1.nextId shouldBe Some(PagingKey.toNextId(page1.recordSets(1), true))
val page2 = repo
- .listRecordSets(Some(okZone.id), page1.nextId, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), page1.nextId, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(page2.recordSets should contain).theSameElementsInOrderAs(
existing
@@ -574,7 +587,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
page2.nextId shouldBe Some(PagingKey.toNextId(page2.recordSets(1), true))
val page3 = repo
- .listRecordSets(Some(okZone.id), page2.nextId, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), page2.nextId, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.ASC)
.unsafeRunSync()
(page3.recordSets should contain).theSameElementsInOrderAs(
existing
@@ -597,7 +610,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
val existing = editedChanges.map(_.recordSet)
val page1 = repo
- .listRecordSets(Some(okZone.id), None, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), None, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(page1.recordSets should contain).theSameElementsInOrderAs(
List(recordSetWithFQDN(existing.head, okZone), recordSetWithFQDN(existing(1), okZone))
@@ -605,7 +618,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
page1.nextId shouldBe Some(PagingKey.toNextId(page1.recordSets.last, true))
val page2 = repo
- .listRecordSets(Some(okZone.id), page1.nextId, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), page1.nextId, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(page2.recordSets should contain).theSameElementsInOrderAs(
List(recordSetWithFQDN(existing(2), okZone), recordSetWithFQDN(existing(3), okZone))
@@ -613,16 +626,16 @@ class MySqlRecordSetRepositoryIntegrationSpec
page2.nextId shouldBe Some(PagingKey.toNextId(page2.recordSets.last, true))
val page3 = repo
- .listRecordSets(Some(okZone.id), page2.nextId, Some(2), None, None, None, NameSort.ASC)
+ .listRecordSets(Some(okZone.id), page2.nextId, Some(2), None, None, None, NameSort.ASC, RecordTypeSort.NONE)
.unsafeRunSync()
(page3.recordSets should contain)
.theSameElementsInOrderAs(List(recordSetWithFQDN(existing(4), okZone)))
page3.nextId shouldBe None
}
- "return applicable recordsets in ascending order when recordNameFilter is given" in {
+ "return applicable recordsets in ascending order respect to record type when recordNameFilter is given and record type sort is ascending" in {
val existing = insert(okZone, 10).map(_.recordSet)
val found = repo
- .listRecordSets(None, None, None, Some("*.ok*"), None, None, NameSort.ASC)
+ .listRecordSets(None, None, None, Some("*.ok*"), None, None, NameSort.ASC, RecordTypeSort.ASC)
.unsafeRunSync()
found.recordSets should contain theSameElementsAs existing.map(
r => recordSetWithFQDN(r, okZone)
@@ -631,7 +644,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
"return applicable recordsets in descending order when recordNameFilter is given and name sort is descending" in {
val existing = insert(okZone, 10).map(_.recordSet)
val found = repo
- .listRecordSets(None, None, None, Some("*.ok*"), None, None, NameSort.DESC)
+ .listRecordSets(None, None, None, Some("*.ok*"), None, None, NameSort.DESC, RecordTypeSort.ASC)
.unsafeRunSync()
found.recordSets should contain theSameElementsAs existing
.map(r => recordSetWithFQDN(r, okZone))
@@ -639,7 +652,7 @@ class MySqlRecordSetRepositoryIntegrationSpec
}
"return no recordsets when no zoneId or recordNameFilter are given" in {
val found =
- repo.listRecordSets(None, None, None, None, None, None, NameSort.ASC).unsafeRunSync()
+ repo.listRecordSets(None, None, None, None, None, None, NameSort.ASC, RecordTypeSort.ASC).unsafeRunSync()
found.recordSets shouldBe empty
}
}
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserChangeRepositoryIntegrationSpec.scala
index e9c5341c2..bd009428c 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserChangeRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserChangeRepositoryIntegrationSpec.scala
@@ -20,6 +20,7 @@ import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach}
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scalikejdbc.DB
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.membership.UserChange.{CreateUser, UpdateUser}
import vinyldns.core.domain.membership.{User, UserChangeRepository}
import vinyldns.mysql.TestMySqlInstance
@@ -31,7 +32,7 @@ class MySqlUserChangeRepositoryIntegrationSpec
with Matchers {
private val repo: UserChangeRepository = TestMySqlInstance.userChangeRepository
- private val user: User = User("user-id", "access-key", "secret-key")
+ private val user: User = User("user-id", "access-key", Encrypted("secret-key"))
private val createUser = CreateUser(user, "creator-id", user.created)
private val updateUser =
UpdateUser(user.copy(userName = "new-username"), "creator-id", user.created, user)
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserRepositoryIntegrationSpec.scala
index 7516649c0..d0a278448 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlUserRepositoryIntegrationSpec.scala
@@ -20,6 +20,7 @@ import org.scalatest._
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import scalikejdbc.DB
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.membership.{LockStatus, User, UserRepository}
import vinyldns.mysql.TestMySqlInstance
@@ -35,15 +36,15 @@ class MySqlUserRepositoryIntegrationSpec
private val testUserIds = (for { i <- 0 to 100 } yield s"test-user-$i").toList.sorted
private val users = testUserIds.map { id =>
- User(id = id, userName = "name" + id, accessKey = s"abc$id", secretKey = "123")
+ User(id = id, userName = "name" + id, accessKey = s"abc$id", secretKey = Encrypted("123"))
}
private val caseInsensitiveUser1 =
- User(id = "caseInsensitiveUser1", userName = "Name1", accessKey = "a1", secretKey = "s1")
+ User(id = "caseInsensitiveUser1", userName = "Name1", accessKey = "a1", secretKey = Encrypted("s1"))
private val caseInsensitiveUser2 =
- User(id = "caseInsensitiveUser2", userName = "namE2", accessKey = "a2", secretKey = "s2")
+ User(id = "caseInsensitiveUser2", userName = "namE2", accessKey = "a2", secretKey = Encrypted("s2"))
private val caseInsensitiveUser3 =
- User(id = "caseInsensitiveUser3", userName = "name3", accessKey = "a3", secretKey = "s3")
+ User(id = "caseInsensitiveUser3", userName = "name3", accessKey = "a3", secretKey = Encrypted("s3"))
override protected def beforeAll(): Unit = {
repo = TestMySqlInstance.userRepository
@@ -81,7 +82,7 @@ class MySqlUserRepositoryIntegrationSpec
}
"save super user with super status" in {
- val superUser = User("superName", "superAccess", "superSecret", isSuper = true)
+ val superUser = User("superName", "superAccess", Encrypted("superSecret"), isSuper = true)
repo.save(superUser).unsafeRunSync() shouldBe superUser
val result = repo.getUser(superUser.id).unsafeRunSync()
result shouldBe Some(superUser)
@@ -89,7 +90,7 @@ class MySqlUserRepositoryIntegrationSpec
}
"save non-super user with non-super status" in {
- val nonSuperUser = User("nonSuperName", "nonSuperAccess", "nonSuperSecret")
+ val nonSuperUser = User("nonSuperName", "nonSuperAccess", Encrypted("nonSuperSecret"))
repo.save(nonSuperUser).unsafeRunSync() shouldBe nonSuperUser
val result = repo.getUser(nonSuperUser.id).unsafeRunSync()
result shouldBe Some(nonSuperUser)
@@ -98,7 +99,7 @@ class MySqlUserRepositoryIntegrationSpec
"save locked user with locked status" in {
val lockedUser =
- User("lockedName", "lockedAccess", "lockedSecret", lockStatus = LockStatus.Locked)
+ User("lockedName", "lockedAccess", Encrypted("lockedSecret"), lockStatus = LockStatus.Locked)
repo.save(lockedUser).unsafeRunSync() shouldBe lockedUser
val result = repo.getUser(lockedUser.id).unsafeRunSync()
result shouldBe Some(lockedUser)
@@ -106,7 +107,7 @@ class MySqlUserRepositoryIntegrationSpec
}
"save unlocked user with unlocked status" in {
- val unlockedUser = User("unlockedName", "unlockedAccess", "unlockedSecret")
+ val unlockedUser = User("unlockedName", "unlockedAccess", Encrypted("unlockedSecret"))
repo.save(unlockedUser).unsafeRunSync() shouldBe unlockedUser
val result = repo.getUser(unlockedUser.id).unsafeRunSync()
result shouldBe Some(unlockedUser)
@@ -114,7 +115,7 @@ class MySqlUserRepositoryIntegrationSpec
}
"save support user with support status" in {
- val supportUser = User("lockedName", "lockedAccess", "lockedSecret", isSupport = true)
+ val supportUser = User("lockedName", "lockedAccess", Encrypted("lockedSecret"), isSupport = true)
repo.save(supportUser).unsafeRunSync() shouldBe supportUser
val result = repo.getUser(supportUser.id).unsafeRunSync()
result shouldBe Some(supportUser)
@@ -122,7 +123,7 @@ class MySqlUserRepositoryIntegrationSpec
}
"save non-support user with non-support status" in {
- val nonSupportUser = User("unlockedName", "unlockedAccess", "unlockedSecret")
+ val nonSupportUser = User("unlockedName", "unlockedAccess", Encrypted("unlockedSecret"))
repo.save(nonSupportUser).unsafeRunSync() shouldBe nonSupportUser
val result = repo.getUser(nonSupportUser.id).unsafeRunSync()
result shouldBe Some(nonSupportUser)
@@ -131,7 +132,7 @@ class MySqlUserRepositoryIntegrationSpec
"save a list of users" in {
val userList = (0 to 10).toList.map { i =>
- User(userName = s"batch-save-user-$i", "accessKey", "secretKey")
+ User(userName = s"batch-save-user-$i", "accessKey", Encrypted("secretKey"))
}
repo.save(userList).unsafeRunSync() shouldBe userList
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala
index fb2bd5dc5..b40c50775 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneChangeRepositoryIntegrationSpec.scala
@@ -31,7 +31,11 @@ import vinyldns.core.domain.zone.ZoneChangeStatus.ZoneChangeStatus
import vinyldns.core.domain.zone._
import vinyldns.core.TestZoneData.okZone
import vinyldns.core.TestZoneData.testConnection
+import vinyldns.core.domain.Encrypted
import vinyldns.mysql.TestMySqlInstance
+import vinyldns.core.TestZoneData.{okZone, testConnection}
+import vinyldns.core.domain.auth.AuthPrincipal
+import vinyldns.core.TestMembershipData.{dummyAuth, dummyGroup, okGroup, okUser}
import scala.concurrent.duration._
import scala.util.Random
@@ -48,6 +52,8 @@ class MySqlZoneChangeRepositoryIntegrationSpec
IO.contextShift(scala.concurrent.ExecutionContext.global)
private var repo: ZoneChangeRepository = _
+ private val zoneRepo= TestMySqlInstance.zoneRepository.asInstanceOf[MySqlZoneRepository]
+
object TestData {
def randomZoneChange: ZoneChange =
@@ -59,12 +65,13 @@ class MySqlZoneChangeRepositoryIntegrationSpec
systemMessage = Some("test")
)
- val goodUser: User = User(s"live-test-acct", "key", "secret")
+ val goodUser: User = User(s"live-test-acct", "key", Encrypted("secret"))
val zones: IndexedSeq[Zone] = for { i <- 1 to 3 } yield Zone(
s"${goodUser.userName}.zone$i.",
"test@test.com",
status = ZoneStatus.Active,
+ adminGroupId = goodUser.id,
connection = testConnection
)
@@ -79,6 +86,102 @@ class MySqlZoneChangeRepositoryIntegrationSpec
status,
created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000))
)
+
+ val failedChanges
+ : IndexedSeq[ZoneChange] = for { zone <- zones } yield ZoneChange(
+ zone,
+ zone.account,
+ ZoneChangeType.Update,
+ status= ZoneChangeStatus.Failed,
+ created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000))
+ )
+
+ val successChanges
+ : IndexedSeq[ZoneChange] = for { zone <- zones } yield ZoneChange(
+ zone,
+ zone.account,
+ ZoneChangeType.Update,
+ status= ZoneChangeStatus.Synced,
+ created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusSeconds(Random.nextInt(1000))
+ )
+
+ val groups = (11 until 20)
+ .map(num => okGroup.copy(name = num.toString, id = UUID.randomUUID().toString))
+ .toList
+
+ // generate some ACLs
+ private val groupAclRules = groups.map(
+ g =>
+ ACLRule(
+ accessLevel = AccessLevel.Read,
+ groupId = Some(g.id)
+ )
+ )
+
+ private val userOnlyAclRule =
+ ACLRule(
+ accessLevel = AccessLevel.Read,
+ userId = Some(okUser.id)
+ )
+
+ // the zone acl rule will have the user rule and all of the group rules
+ private val testZoneAcl = ZoneACL(
+ rules = Set(userOnlyAclRule) ++ groupAclRules
+ )
+
+ private val testZoneAdminGroupId = "foo"
+
+ val dummyAclRule =
+ ACLRule(
+ accessLevel = AccessLevel.Read,
+ groupId = Some(dummyGroup.id)
+ )
+
+ val testZone = (11 until 20).map { num =>
+ val z =
+ okZone.copy(
+ name = num.toString + ".",
+ id = UUID.randomUUID().toString,
+ adminGroupId = testZoneAdminGroupId,
+ acl = testZoneAcl
+ )
+ // add the dummy acl rule to the first zone
+ if (num == 11) z.addACLRule(dummyAclRule) else z
+ }
+
+ val deletedZoneChanges
+ : IndexedSeq[ZoneChange] = for { testZone <- testZone } yield {
+ ZoneChange(
+ testZone.copy(status = ZoneStatus.Deleted),
+ testZone.account,
+ ZoneChangeType.Create,
+ ZoneChangeStatus.Synced,
+ created = Instant.now.truncatedTo(ChronoUnit.MILLIS).minusMillis(1000)
+ )}
+
+ def saveZones(zones: Seq[Zone]): IO[Unit] =
+ zones.foldLeft(IO.unit) {
+ case (acc, z) =>
+ acc.flatMap { _ =>
+ zoneRepo.save(z).map(_ => ())
+ }
+ }
+
+ def deleteZones(zones: Seq[Zone]): IO[Unit] =
+ zones.foldLeft(IO.unit) {
+ case (acc, z) =>
+ acc.flatMap { _ =>
+ zoneRepo.deleteTx(z).map(_ => ())
+ }
+ }
+
+ def saveZoneChanges(zoneChanges: Seq[ZoneChange]): IO[Unit] =
+ zoneChanges.foldLeft(IO.unit) {
+ case (acc, zc) =>
+ acc.flatMap { _ =>
+ repo.save(zc).map(_ => ())
+ }
+ }
}
import TestData._
@@ -147,6 +250,75 @@ class MySqlZoneChangeRepositoryIntegrationSpec
listResponse.startFrom should equal(None)
}
+ "get all failedChanges for a failed zone changes with and without StartFrom, MaxItems" in {
+ zones.map(zoneRepo.save(_)).toList.parSequence.unsafeRunTimed(5.minutes)
+ .getOrElse(
+ fail("timeout waiting for changes to save in MySqlZoneChangeRepositoryIntegrationSpec")
+ )
+
+ val changeSetupResults = failedChanges.map(repo.save(_)).toList.parSequence
+ changeSetupResults
+ .unsafeRunTimed(5.minutes)
+ .getOrElse(
+ fail("timeout waiting for changes to save in MySqlZoneChangeRepositoryIntegrationSpec")
+ )
+
+ val expectedChanges =
+ failedChanges.sortBy(_.created.toEpochMilli).reverse.toList
+
+ val listResponse = repo.listFailedZoneChanges(100,0).unsafeRunSync()
+ listResponse.items should equal(expectedChanges)
+ listResponse.nextId should equal(0)
+ listResponse.startFrom should equal(0)
+ listResponse.maxItems should equal(100)
+
+ val listResponse1 = repo.listFailedZoneChanges(2,1).unsafeRunSync()
+ listResponse1.items.size should equal(2)
+ listResponse1.nextId should equal(3)
+ listResponse1.startFrom should equal(1)
+ listResponse1.maxItems should equal(2)
+
+ val expectedPageOne = List(expectedChanges(0))
+ val expectedPageTwo = List(expectedChanges(1))
+ val expectedPageThree = List(expectedChanges(2))
+
+ val pageOne =
+ repo.listFailedZoneChanges(maxItems = 1, startFrom = 0).unsafeRunSync()
+ pageOne.items.size should equal(1)
+ pageOne.items should equal(expectedPageOne)
+ pageOne.nextId should equal(1)
+ pageOne.startFrom should equal(0)
+
+ // get second page
+ val pageTwo =
+ repo.listFailedZoneChanges(maxItems = 1, startFrom = pageOne.nextId).unsafeRunSync()
+ pageTwo.items.size should equal(1)
+ pageTwo.items should equal(expectedPageTwo)
+ pageTwo.nextId should equal(2)
+ pageTwo.startFrom should equal(pageOne.nextId)
+
+ // get final page
+ // next id should be none now
+ val pageThree =
+ repo.listFailedZoneChanges( maxItems = 1, startFrom = pageTwo.nextId).unsafeRunSync()
+ pageThree.items.size should equal(1)
+ pageThree.items should equal(expectedPageThree)
+ pageThree.nextId should equal(3)
+ pageThree.startFrom should equal(pageTwo.nextId)
+ }
+
+ "get empty list in failedChanges for a success zone changes" in {
+ val changeSetupResults = successChanges.map(repo.save(_)).toList.parSequence
+ changeSetupResults
+ .unsafeRunTimed(5.minutes)
+ .getOrElse(
+ fail("timeout waiting for changes to save in MySqlZoneChangeRepositoryIntegrationSpec")
+ )
+
+ val listResponse = repo.listFailedZoneChanges(100).unsafeRunSync()
+ listResponse shouldBe ListFailedZoneChangesResults(List(),0,0,100)
+ }
+
"get zone changes using a maxItems of 1" in {
val changeSetupResults = changes.map(repo.save(_)).toList.parSequence
changeSetupResults
@@ -213,5 +385,145 @@ class MySqlZoneChangeRepositoryIntegrationSpec
pageThree.nextId should equal(None)
pageThree.startFrom should equal(pageTwo.nextId)
}
+ "get authorized zones" in {
+ // store all of the zones
+ saveZones(testZone).unsafeRunSync()
+ // delete all stored zones
+ deleteZones(testZone).unsafeRunSync()
+ // save the change
+ saveZoneChanges(deletedZoneChanges).unsafeRunSync()
+
+ // query for all zones for the ok user, he should have access to all of the zones
+ val okUserAuth = AuthPrincipal(
+ signedInUser = okUser,
+ memberGroupIds = groups.map(_.id)
+ )
+
+ repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs deletedZoneChanges
+
+ // dummy user only has access to one zone
+ (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(deletedZoneChanges.head)
+ }
+
+ "page deleted zones using a startFrom and maxItems" in {
+ // store all of the zones
+ saveZones(testZone).unsafeRunSync()
+ // delete all stored zones
+ deleteZones(testZone).unsafeRunSync()
+ // save the change
+ saveZoneChanges(deletedZoneChanges).unsafeRunSync()
+
+ // query for all zones for the ok user, he should have access to all of the zones
+ val okUserAuth = AuthPrincipal(
+ signedInUser = okUser,
+ memberGroupIds = groups.map(_.id)
+ )
+
+ val listDeletedZones = repo.listDeletedZones(okUserAuth).unsafeRunSync()
+
+ val expectedPageOne = List(listDeletedZones.zoneDeleted(0))
+ val expectedPageOneNext = Some(listDeletedZones.zoneDeleted(1).zone.id)
+ val expectedPageTwo = List(listDeletedZones.zoneDeleted(1))
+ val expectedPageTwoNext = Some(listDeletedZones.zoneDeleted(2).zone.id)
+ val expectedPageThree = List(listDeletedZones.zoneDeleted(2))
+ val expectedPageThreeNext = Some(listDeletedZones.zoneDeleted(3).zone.id)
+
+ // get first page
+ val pageOne = repo.listDeletedZones(okUserAuth,startFrom = None, maxItems = 1 ).unsafeRunSync()
+ pageOne.zoneDeleted.size should equal(1)
+ pageOne.zoneDeleted should equal(expectedPageOne)
+ pageOne.nextId should equal(expectedPageOneNext)
+ pageOne.startFrom should equal(None)
+
+ // get second page
+ val pageTwo =
+ repo.listDeletedZones(okUserAuth, startFrom = pageOne.nextId, maxItems = 1).unsafeRunSync()
+ pageTwo.zoneDeleted.size should equal(1)
+ pageTwo.zoneDeleted should equal(expectedPageTwo)
+ pageTwo.nextId should equal(expectedPageTwoNext)
+ pageTwo.startFrom should equal(pageOne.nextId)
+
+ // get final page
+ // next id should be none now
+ val pageThree =
+ repo.listDeletedZones(okUserAuth, startFrom = pageTwo.nextId, maxItems = 1).unsafeRunSync()
+ pageThree.zoneDeleted.size should equal(1)
+ pageThree.zoneDeleted should equal(expectedPageThree)
+ pageThree.nextId should equal(expectedPageThreeNext)
+ pageThree.startFrom should equal(pageTwo.nextId)
+ }
+
+ "return empty in deleted zone if zone is created again" in {
+ // store all of the zones
+ saveZones(testZone).unsafeRunSync()
+ // save the change
+ saveZoneChanges(deletedZoneChanges).unsafeRunSync()
+
+ // query for all zones for the ok user, he should have access to all of the zones
+ val okUserAuth = AuthPrincipal(
+ signedInUser = okUser,
+ memberGroupIds = groups.map(_.id)
+ )
+
+ repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain theSameElementsAs List()
+
+ // delete all stored zones
+ deleteZones(testZone).unsafeRunSync()
+
+ }
+
+ "return an empty list of zones if the user is not authorized to any" in {
+ val unauthorized = AuthPrincipal(
+ signedInUser = User("not-authorized", "not-authorized", Encrypted("not-authorized")),
+ memberGroupIds = Seq.empty
+ )
+
+ val f =
+ for {
+ _ <- saveZones(testZone)
+ _ <- deleteZones(testZone)
+ _ <- saveZoneChanges(deletedZoneChanges)
+ zones <- repo.listDeletedZones(unauthorized)
+ } yield zones
+
+ f.unsafeRunSync().zoneDeleted shouldBe empty
+ deleteZones(testZone).unsafeRunSync()
+
+ }
+
+ "not return zones when access is revoked" in {
+ // ok user can access both zones, dummy can only access first zone
+ val zones = testZone.take(2)
+ val zoneChange = deletedZoneChanges.take(2)
+ val addACL = saveZones(zones)
+ val deleteZone= deleteZones(zones)
+ val addACLZc = saveZoneChanges(zoneChange)
+
+
+ val okUserAuth = AuthPrincipal(
+ signedInUser = okUser,
+ memberGroupIds = groups.map(_.id)
+ )
+ addACL.unsafeRunSync()
+ deleteZone.unsafeRunSync()
+ addACLZc.unsafeRunSync()
+
+ (repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain). allElementsOf(zoneChange)
+
+ // dummy user only has access to first zone
+ (repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted should contain).only(zoneChange.head)
+
+ // revoke the access for the dummy user
+ val revoked = zones(0).deleteACLRule(dummyAclRule)
+ val revokedZc = zoneChange(0).copy(zone=revoked)
+ zoneRepo.save(revoked).unsafeRunSync()
+ repo.save(revokedZc).unsafeRunSync()
+
+ // ok user can still access zones
+ (repo.listDeletedZones(okUserAuth).unsafeRunSync().zoneDeleted should contain).allElementsOf(Seq( zoneChange(1)))
+
+ // dummy user can not access the revoked zone
+ repo.listDeletedZones(dummyAuth).unsafeRunSync().zoneDeleted shouldBe empty
+ }
}
}
diff --git a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala
index 8daa0e58b..9c54eef8f 100644
--- a/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala
+++ b/modules/mysql/src/it/scala/vinyldns/mysql/repository/MySqlZoneRepositoryIntegrationSpec.scala
@@ -28,6 +28,7 @@ import vinyldns.core.domain.membership.User
import vinyldns.core.domain.zone._
import vinyldns.core.TestZoneData.okZone
import vinyldns.core.TestMembershipData._
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.zone.ZoneRepository.DuplicateZoneError
import vinyldns.mysql.{TestMySqlInstance, TransactionProvider}
import vinyldns.mysql.TestMySqlInstance.groupRepository
@@ -340,6 +341,57 @@ class MySqlZoneRepositoryIntegrationSpec
groupRepository.delete(okGroup).unsafeRunSync()
}
+ "check pagination while filtering zones by admin group" in {
+
+ executeWithinTransaction { db: DB =>
+ groupRepository.save(db, okGroup)
+ }.unsafeRunSync()
+
+ val group = groupRepository.getGroupsByName(okGroup.name).unsafeRunSync()
+ val groupId = group.head.id
+
+ // store all of the zones
+ val privateZone = okZone.copy(
+ name = "private-zone.",
+ id = UUID.randomUUID().toString,
+ acl = ZoneACL(),
+ adminGroupId = groupId
+ )
+
+ val sharedZone = okZone.copy(
+ name = "shared-zone.",
+ id = UUID.randomUUID().toString,
+ acl = ZoneACL(),
+ shared = true,
+ adminGroupId = groupId
+ )
+
+ val testZones = Seq(privateZone, sharedZone)
+
+ val f = saveZones(testZones)
+
+ // query for all zones for the ok user, should have all of the zones returned
+ val okUserAuth = AuthPrincipal(
+ signedInUser = okUser,
+ memberGroupIds = groups.map(_.id)
+ )
+
+ f.unsafeRunSync()
+
+ val page1 = repo
+ .listZonesByAdminGroupIds(okUserAuth, None, 1, Set(groupId), ignoreAccess = true)
+ .unsafeRunSync()
+ page1.zones.head shouldBe testZones.head
+
+ val page2 = repo
+ .listZonesByAdminGroupIds(okUserAuth, page1.nextId, 1, Set(groupId), ignoreAccess = true)
+ .unsafeRunSync()
+ page2.zones.head shouldBe testZones.last
+
+ // delete the group created to test
+ groupRepository.delete(okGroup).unsafeRunSync()
+ }
+
"get empty list when no matching admin group name is found while filtering zones by group name" in {
executeWithinTransaction { db: DB =>
@@ -435,7 +487,7 @@ class MySqlZoneRepositoryIntegrationSpec
"return an empty list of zones if the user is not authorized to any" in {
val unauthorized = AuthPrincipal(
- signedInUser = User("not-authorized", "not-authorized", "not-authorized"),
+ signedInUser = User("not-authorized", "not-authorized", Encrypted("not-authorized")),
memberGroupIds = Seq.empty
)
@@ -551,10 +603,53 @@ class MySqlZoneRepositoryIntegrationSpec
f.unsafeRunSync().zones should contain theSameElementsAs expectedZones
}
+ "apply the reverse zone filter as a super user" in {
+
+ val testZones = Seq(
+ testZone("system-test."),
+ testZone("system-test.ip6.arpa."),
+ testZone("system-temp.in-addr.arpa."),
+ testZone("nomatch.in-addr.arpa.")
+ )
+
+ val expectedZones = Seq(testZones(0))
+
+ val f =
+ for {
+ _ <- saveZones(testZones)
+ retrieved <- repo.listZones(superUserAuth, includeReverse = false)
+ } yield retrieved
+
+ f.unsafeRunSync().zones should contain theSameElementsAs expectedZones
+ }
+
+ "apply the zone filter and reverse zone filter as a super user" in {
+
+ val testZones = Seq(
+ testZone("system-test."),
+ testZone("system-temp.ip6.arpa."),
+ testZone("system-test.ip6.arpa."),
+ testZone("system-temp.in-addr.arpa."),
+ testZone("no-match.")
+ )
+
+ val expectedZones = Seq(testZones(0)).sortBy(_.name)
+
+ val auth = AuthPrincipal(dummyUser, Seq("foo"))
+
+ val f =
+ for {
+ _ <- saveZones(testZones)
+ retrieved <- repo.listZones(auth, zoneNameFilter = Some("system*"), includeReverse = false)
+ } yield retrieved
+
+ (f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones)
+ }
+
"apply the zone filter as a normal user" in {
val testZones = Seq(
- testZone("system-test.", adminGroupId = "foo"),
+ testZone("system-test.ip6.arpa.", adminGroupId = "foo"),
testZone("system-temp.", adminGroupId = "foo"),
testZone("system-nomatch.", adminGroupId = "bar")
)
@@ -593,6 +688,52 @@ class MySqlZoneRepositoryIntegrationSpec
(f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones)
}
+ "apply the zone filter and reverse zone filter as a normal user" in {
+
+ val testZones = Seq(
+ testZone("system-test.ip6.arpa.", adminGroupId = "foo"),
+ testZone("system-temp.", adminGroupId = "foo"),
+ testZone("system-temp.in-addr.arpa.", adminGroupId = "foo"),
+ testZone("system-nomatch.", adminGroupId = "bar")
+ )
+
+ val expectedZones = Seq(testZones(1)).sortBy(_.name)
+
+ val auth = AuthPrincipal(dummyUser, Seq("foo"))
+
+ val f =
+ for {
+ _ <- saveZones(testZones)
+ retrieved <- repo.listZones(auth, zoneNameFilter = Some("system*"), includeReverse = false)
+ } yield retrieved
+
+ (f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones)
+ }
+
+ "apply the reverse zone filter as a normal user" in {
+
+ val testZones = Seq(
+ testZone("system-test.ip6.arpa.", adminGroupId = "foo"),
+ testZone("system-test.in-addr.arpa.", adminGroupId = "foo"),
+ testZone("system-temp.in-addr.arpa.", adminGroupId = "foo"),
+ testZone("system-match.", adminGroupId = "foo"),
+ testZone("system-nomatch.", adminGroupId = "bar"),
+ testZone("system-nomatch.in-addr.arpa.", adminGroupId = "bar")
+ )
+
+ val expectedZones = Seq(testZones(3)).sortBy(_.name)
+
+ val auth = AuthPrincipal(dummyUser, Seq("foo"))
+
+ val f =
+ for {
+ _ <- saveZones(testZones)
+ retrieved <- repo.listZones(auth, includeReverse = false)
+ } yield retrieved
+
+ (f.unsafeRunSync().zones should contain).theSameElementsInOrderAs(expectedZones)
+ }
+
"support starts with wildcard" in {
val testZones = Seq(
@@ -811,12 +952,13 @@ class MySqlZoneRepositoryIntegrationSpec
"check if an id has an ACL rule for at least one of the zones" in {
val zoneId = UUID.randomUUID().toString
+ val adminId = UUID.randomUUID().toString
val testZones = (1 until 3).map { num =>
okZone.copy(
name = num.toString + ".",
id = zoneId,
- adminGroupId = testZoneAdminGroupId,
+ adminGroupId = adminId,
acl = testZoneAcl
)
}
@@ -824,7 +966,7 @@ class MySqlZoneRepositoryIntegrationSpec
val f =
for {
_ <- saveZones(testZones)
- zones <- repo.getFirstOwnedZoneAclGroupId(testZoneAdminGroupId)
+ zones <- repo.getFirstOwnedZoneAclGroupId(adminId)
} yield zones
f.unsafeRunSync() shouldBe Some(zoneId)
@@ -848,5 +990,21 @@ class MySqlZoneRepositoryIntegrationSpec
f.unsafeRunSync() shouldBe None
}
+
+ "return zones which have zone sync scheduled" in {
+ // okZone with recurrence schedule
+ repo.save(okZone).unsafeRunSync() shouldBe Right(okZone)
+ val updatedOkZone = okZone.copy(recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ repo.save(updatedOkZone).unsafeRunSync() shouldBe Right(updatedOkZone)
+ repo.getZoneByName(updatedOkZone.name).unsafeRunSync().get.recurrenceSchedule shouldBe Some("0/5 0 0 ? * * *")
+
+ // dummyZone without recurrence schedule
+ val dummyZone = okZone.copy(name = "dummy.", id = "5615c19c-cb00-4734-9acd-fbfdca0e6fce")
+ repo.save(dummyZone).unsafeRunSync() shouldBe Right(dummyZone)
+ repo.getZoneByName(dummyZone.name).unsafeRunSync().get.recurrenceSchedule shouldBe None
+
+ // Only get zone with recurrence schedule
+ repo.getAllZonesWithSyncSchedule.unsafeRunSync() shouldBe Set(updatedOkZone)
+ }
}
}
diff --git a/modules/mysql/src/main/resources/db/migration/V3.22__AddGroupFields.sql b/modules/mysql/src/main/resources/db/migration/V3.22__AddGroupFields.sql
index a6ac672b9..90ff7fbc7 100644
--- a/modules/mysql/src/main/resources/db/migration/V3.22__AddGroupFields.sql
+++ b/modules/mysql/src/main/resources/db/migration/V3.22__AddGroupFields.sql
@@ -2,6 +2,6 @@ CREATE SCHEMA IF NOT EXISTS ${dbName};
USE ${dbName};
-ALTER TABLE groups ADD COLUMN description VARCHAR(256) NULL;
-ALTER TABLE groups ADD COLUMN created_timestamp DATETIME NOT NULL;
-ALTER TABLE groups ADD COLUMN email VARCHAR(256) NOT NULL;
+ALTER TABLE `groups` ADD COLUMN description VARCHAR(256) NULL;
+ALTER TABLE `groups` ADD COLUMN created_timestamp DATETIME NOT NULL;
+ALTER TABLE `groups` ADD COLUMN email VARCHAR(256) NOT NULL;
diff --git a/modules/mysql/src/main/resources/db/migration/V3.28__ZoneSchedule.sql b/modules/mysql/src/main/resources/db/migration/V3.28__ZoneSchedule.sql
new file mode 100644
index 000000000..c764fcdec
--- /dev/null
+++ b/modules/mysql/src/main/resources/db/migration/V3.28__ZoneSchedule.sql
@@ -0,0 +1,5 @@
+CREATE SCHEMA IF NOT EXISTS ${dbName};
+
+USE ${dbName};
+
+ALTER TABLE zone ADD zone_sync_schedule VARCHAR(256) NULL;
diff --git a/modules/mysql/src/main/resources/db/migration/V3.29__AddRecordChangeFields.sql b/modules/mysql/src/main/resources/db/migration/V3.29__AddRecordChangeFields.sql
new file mode 100644
index 000000000..cac0452fd
--- /dev/null
+++ b/modules/mysql/src/main/resources/db/migration/V3.29__AddRecordChangeFields.sql
@@ -0,0 +1,8 @@
+CREATE SCHEMA IF NOT EXISTS ${dbName};
+
+USE ${dbName};
+
+ALTER TABLE record_change ADD COLUMN fqdn VARCHAR(255) NOT NULL;
+ALTER TABLE record_change ADD COLUMN record_type VARCHAR(255) NOT NULL;
+CREATE INDEX fqdn_index ON record_change (fqdn);
+CREATE INDEX record_type_index ON record_change (record_type);
diff --git a/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql b/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql
new file mode 100644
index 000000000..cb6337994
--- /dev/null
+++ b/modules/mysql/src/main/resources/db/migration/V3.30__AddStatusZoneAccess.sql
@@ -0,0 +1,7 @@
+CREATE SCHEMA IF NOT EXISTS ${dbName};
+
+USE ${dbName};
+
+ALTER TABLE zone_access
+DROP FOREIGN KEY fk_zone_access,
+ADD COLUMN zone_status CHAR(36) NOT NULL;
\ No newline at end of file
diff --git a/modules/mysql/src/main/resources/db/migration/V3.31__AddZoneChangeFields.sql b/modules/mysql/src/main/resources/db/migration/V3.31__AddZoneChangeFields.sql
new file mode 100644
index 000000000..7246e7bb4
--- /dev/null
+++ b/modules/mysql/src/main/resources/db/migration/V3.31__AddZoneChangeFields.sql
@@ -0,0 +1,12 @@
+CREATE SCHEMA IF NOT EXISTS ${dbName};
+
+USE ${dbName};
+
+ALTER TABLE zone_change
+ADD COLUMN zone_name VARCHAR(256) NOT NULL;
+CREATE INDEX zone_name_index ON zone_change(zone_name);
+
+
+ALTER TABLE zone_change
+ADD COLUMN zone_status CHAR(36) NOT NULL;
+CREATE INDEX zone_status_index ON zone_change(zone_status);
\ No newline at end of file
diff --git a/modules/mysql/src/main/resources/db/migration/V3.32__BatchStatus.sql b/modules/mysql/src/main/resources/db/migration/V3.32__BatchStatus.sql
new file mode 100644
index 000000000..2979d002f
--- /dev/null
+++ b/modules/mysql/src/main/resources/db/migration/V3.32__BatchStatus.sql
@@ -0,0 +1,5 @@
+CREATE SCHEMA IF NOT EXISTS ${dbName};
+
+USE ${dbName};
+
+ALTER TABLE batch_change ADD batch_status VARCHAR(25) NOT NULL;
diff --git a/modules/mysql/src/main/resources/db/migration/V3.4__Groups.sql b/modules/mysql/src/main/resources/db/migration/V3.4__Groups.sql
index 0619e04b3..2e01f2d3e 100644
--- a/modules/mysql/src/main/resources/db/migration/V3.4__Groups.sql
+++ b/modules/mysql/src/main/resources/db/migration/V3.4__Groups.sql
@@ -5,7 +5,7 @@ USE ${dbName};
/*
Create table to store groups
*/
-CREATE TABLE groups (
+CREATE TABLE `groups` (
id CHAR(36) NOT NULL,
name VARCHAR(256) NOT NULL,
data BLOB NOT NULL,
diff --git a/modules/mysql/src/main/resources/test/ddl.sql b/modules/mysql/src/main/resources/test/ddl.sql
index 4889127a9..d310c48d0 100644
--- a/modules/mysql/src/main/resources/test/ddl.sql
+++ b/modules/mysql/src/main/resources/test/ddl.sql
@@ -237,10 +237,7 @@ CREATE TABLE IF NOT EXISTS zone_access
(
accessor_id char(36) not null,
zone_id char(36) not null,
- primary key (accessor_id, zone_id),
- constraint fk_zone_access_zone_id
- foreign key (zone_id) references zone (id)
- on delete cascade
+ primary key (accessor_id, zone_id)
);
CREATE INDEX IF NOT EXISTS zone_access_accessor_id_index
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/TransactionProvider.scala b/modules/mysql/src/main/scala/vinyldns/mysql/TransactionProvider.scala
index 72ca80cde..338bc8360 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/TransactionProvider.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/TransactionProvider.scala
@@ -19,7 +19,7 @@ package vinyldns.mysql
import cats.effect.IO
import org.slf4j.{Logger, LoggerFactory}
import scalikejdbc.{ConnectionPool, DB}
-
+import java.io.{PrintWriter, StringWriter}
import java.sql.SQLException
import java.util.UUID
@@ -52,7 +52,9 @@ trait TransactionProvider {
result
} catch {
case e: Throwable =>
- logger.error(s"Encountered error executing function within a database transaction ($txId). Rolling back transaction.", e)
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Encountered error executing function within a database transaction ($txId). Rolling back transaction. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
db.rollbackIfActive()
throw e
} finally {
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/queue/MySqlMessageQueue.scala b/modules/mysql/src/main/scala/vinyldns/mysql/queue/MySqlMessageQueue.scala
index 8f8e77bcb..d7f7d9c9a 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/queue/MySqlMessageQueue.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/queue/MySqlMessageQueue.scala
@@ -34,6 +34,7 @@ import vinyldns.mysql.queue.MessageType.{
RecordChangeMessageType,
ZoneChangeMessageType
}
+import java.io.{PrintWriter, StringWriter}
import vinyldns.proto.VinylDNSProto
import java.time.temporal.ChronoUnit
import scala.concurrent.duration._
@@ -210,7 +211,9 @@ class MySqlMessageQueue(maxRetries: Int)
// Errors could not be deserialized, have an invalid type, or exceeded retries
val errors = claimed.collect {
case Left((e, id)) =>
- logger.error(s"Encountered error for message with id $id", e)
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Encountered error for message with id $id. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
id
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlBatchChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlBatchChangeRepository.scala
index d83c0de50..ef5d9fa7b 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlBatchChangeRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlBatchChangeRepository.scala
@@ -17,13 +17,14 @@
package vinyldns.mysql.repository
import java.sql.Timestamp
-
import cats.data._
import cats.effect._
+
import java.time.Instant
import org.slf4j.LoggerFactory
import scalikejdbc._
import vinyldns.core.domain.batch.BatchChangeApprovalStatus.BatchChangeApprovalStatus
+import vinyldns.core.domain.batch.BatchChangeStatus.BatchChangeStatus
import vinyldns.core.domain.batch._
import vinyldns.core.protobuf.{BatchChangeProtobufConversions, SingleChangeType}
import vinyldns.core.route.Monitored
@@ -48,11 +49,11 @@ class MySqlBatchChangeRepository
private final val PUT_BATCH_CHANGE =
sql"""
| INSERT INTO batch_change(id, user_id, user_name, created_time, comments, owner_group_id,
- | approval_status, reviewer_id, review_comment, review_timestamp,
+ | approval_status, batch_status, reviewer_id, review_comment, review_timestamp,
| scheduled_time, cancelled_timestamp)
| VALUES ({id}, {userId}, {userName}, {createdTime}, {comments}, {ownerGroupId}, {approvalStatus},
- | {reviewerId}, {reviewComment}, {reviewTimestamp}, {scheduledTime}, {cancelledTimestamp})
- |ON DUPLICATE KEY UPDATE comments={comments}, owner_group_id={ownerGroupId}, approval_status={approvalStatus},
+ | {batchStatus}, {reviewerId}, {reviewComment}, {reviewTimestamp}, {scheduledTime}, {cancelledTimestamp})
+ |ON DUPLICATE KEY UPDATE comments={comments}, owner_group_id={ownerGroupId}, approval_status={approvalStatus}, batch_status={batchStatus},
| reviewer_id={reviewerId}, review_comment={reviewComment}, review_timestamp={reviewTimestamp},
| scheduled_time={scheduledTime}, cancelled_timestamp={cancelledTimestamp}
""".stripMargin
@@ -70,14 +71,14 @@ class MySqlBatchChangeRepository
private final val GET_BATCH_CHANGE_METADATA =
sql"""
|SELECT user_id, user_name, created_time, comments, owner_group_id,
- | approval_status, reviewer_id, review_comment, review_timestamp, scheduled_time, cancelled_timestamp
+ | approval_status, batch_status, reviewer_id, review_comment, review_timestamp, scheduled_time, cancelled_timestamp
| FROM batch_change bc
| WHERE bc.id = ?
""".stripMargin
private final val GET_BATCH_CHANGE_METADATA_FROM_SINGLE_CHANGE =
sql"""
- |SELECT bc.id, bc.user_id, bc.user_name, bc.created_time, bc.comments, bc.owner_group_id, bc.approval_status,
+ |SELECT bc.id, bc.user_id, bc.user_name, bc.created_time, bc.comments, bc.owner_group_id, bc.approval_status, bc.batch_status,
| bc.reviewer_id, bc.review_comment, bc.review_timestamp, bc.scheduled_time, bc.cancelled_timestamp
| FROM batch_change bc
| JOIN (SELECT id, batch_change_id from single_change where id = ?) sc
@@ -86,13 +87,13 @@ class MySqlBatchChangeRepository
private final val GET_BATCH_CHANGE_SUMMARY_BASE =
"""
- |SELECT batch_change_page.id, user_id, user_name, created_time, comments, owner_group_id, approval_status, reviewer_id,
+ |SELECT batch_change_page.id, user_id, user_name, created_time, comments, owner_group_id, approval_status, batch_status, reviewer_id,
| review_comment, review_timestamp, scheduled_time, cancelled_timestamp,
| SUM(CASE WHEN sc.status LIKE 'Failed' OR sc.status LIKE 'Rejected' THEN 1 ELSE 0 END) AS fail_count,
| SUM(CASE WHEN sc.status LIKE 'Pending' OR sc.status LIKE 'NeedsReview' THEN 1 ELSE 0 END) AS pending_count,
| SUM(CASE sc.status WHEN 'Complete' THEN 1 ELSE 0 END) AS complete_count,
| SUM(CASE sc.status WHEN 'Cancelled' THEN 1 ELSE 0 END) AS cancelled_count
- | FROM (SELECT bc.id, bc.user_id, bc.user_name, bc.created_time, bc.comments, bc.owner_group_id, bc.approval_status,
+ | FROM (SELECT bc.id, bc.user_id, bc.user_name, bc.created_time, bc.comments, bc.owner_group_id, bc.approval_status, bc.batch_status,
| bc.reviewer_id, bc.review_comment, bc.review_timestamp, bc.scheduled_time, bc.cancelled_timestamp
| FROM batch_change bc
""".stripMargin
@@ -124,6 +125,13 @@ class MySqlBatchChangeRepository
| WHERE id={id}
""".stripMargin
+ private final val UPDATE_BATCH_CHANGE =
+ sql"""
+ |UPDATE batch_change
+ | SET batch_status={batchStatus}
+ | WHERE id={id}
+ """.stripMargin
+
def save(batch: BatchChange): IO[BatchChange] =
monitor("repo.BatchChangeJDBC.save") {
IO {
@@ -166,8 +174,8 @@ class MySqlBatchChangeRepository
}
def getBatchFromSingleChangeId(
- singleChangeId: String
- )(implicit s: DBSession): Option[BatchChange] =
+ singleChangeId: String
+ )(implicit s: DBSession): Option[BatchChange] =
GET_BATCH_CHANGE_METADATA_FROM_SINGLE_CHANGE
.bind(singleChangeId)
.map(extractBatchChange(None))
@@ -182,20 +190,36 @@ class MySqlBatchChangeRepository
batchMeta.copy(changes = changes)
}
+ var failCount = 0
+ var pendingCount = 0
+ var completeCount = 0
+ var cancelledCount = 0
+
+ singleChanges.foreach { sc =>
+ if (sc.status.toString == "Failed" || sc.status.toString == "Rejected") {
+ failCount += 1
+ } else if (sc.status.toString == "Pending" || sc.status.toString == "NeedsReview") {
+ pendingCount += 1
+ } else if (sc.status.toString == "Complete") {
+ completeCount += 1
+ } else {
+ cancelledCount += 1
+ }
+ }
+
monitor("repo.BatchChangeJDBC.updateSingleChanges") {
IO {
- logger.info(
- s"Updating single change statuses: ${singleChanges.map(ch => (ch.id, ch.status))}"
- )
+ logger.info(s"Updating single change status: ${singleChanges.map(ch => (ch.id, ch.status))}")
DB.localTx { implicit s =>
for {
headChange <- singleChanges.headOption
batchParams = singleChanges.map(convertSingleChangeToParams)
_ = UPDATE_SINGLE_CHANGE.batchByName(batchParams: _*).apply()
batchChange <- getBatchFromSingleChangeId(headChange.id)
+ batchStatus = BatchChangeStatus.calculateBatchStatus(batchChange.approvalStatus, pendingCount > 0, failCount > 0, completeCount > 0, batchChange.scheduledTime.isDefined)
+ _ = UPDATE_BATCH_CHANGE.bindByName('batchStatus -> batchStatus.toString, 'id -> batchChange.id).update().apply()
} yield batchChange
- }
- }
+ }}
}
}
@@ -233,8 +257,12 @@ class MySqlBatchChangeRepository
def getBatchChangeSummaries(
userId: Option[String],
+ userName: Option[String] = None,
+ dateTimeStartRange: Option[String] = None,
+ dateTimeEndRange: Option[String] = None,
startFrom: Option[Int] = None,
maxItems: Int = 100,
+ batchStatus: Option[BatchChangeStatus],
approvalStatus: Option[BatchChangeApprovalStatus]
): IO[BatchChangeSummaryList] =
monitor("repo.BatchChangeJDBC.getBatchChangeSummaries") {
@@ -246,7 +274,14 @@ class MySqlBatchChangeRepository
val uid = userId.map(u => s"bc.user_id = '$u'")
val as = approvalStatus.map(a => s"bc.approval_status = '${fromApprovalStatus(a)}'")
- val opts = uid ++ as
+ val bs = batchStatus.map(b => s"bc.batch_status = '${fromBatchStatus(b)}'")
+ val uname = userName.map(uname => s"bc.user_name = '$uname'")
+ val dtRange = if(dateTimeStartRange.isDefined && dateTimeEndRange.isDefined) {
+ Some(s"(bc.created_time >= '${dateTimeStartRange.get}' AND bc.created_time <= '${dateTimeEndRange.get}')")
+ } else {
+ None
+ }
+ val opts = uid ++ as ++ bs ++ uname ++ dtRange
if (opts.nonEmpty) sb.append("WHERE ").append(opts.mkString(" AND "))
@@ -303,6 +338,7 @@ class MySqlBatchChangeRepository
nextId,
maxItems,
ignoreAccess,
+ batchStatus,
approvalStatus
)
}
@@ -345,6 +381,33 @@ class MySqlBatchChangeRepository
private def saveBatchChange(
batchChange: BatchChange
)(implicit session: DBSession): BatchChange = {
+
+ var failCount = 0
+ var pendingCount = 0
+ var completeCount = 0
+ var cancelledCount = 0
+
+ batchChange.changes.foreach { sc =>
+ if (sc.status.toString == "Failed" || sc.status.toString == "Rejected") {
+ failCount += 1
+ } else if (sc.status.toString == "Pending" || sc.status.toString == "NeedsReview") {
+ pendingCount += 1
+ } else if (sc.status.toString == "Complete") {
+ completeCount += 1
+ } else {
+ cancelledCount += 1
+ }
+ }
+
+ val batchStatus = BatchChangeStatus
+ .calculateBatchStatus(
+ batchChange.approvalStatus,
+ pendingCount > 0,
+ failCount > 0,
+ completeCount > 0,
+ batchChange.scheduledTime.isDefined
+ )
+
PUT_BATCH_CHANGE
.bindByName(
Seq(
@@ -355,6 +418,7 @@ class MySqlBatchChangeRepository
'comments -> batchChange.comments,
'ownerGroupId -> batchChange.ownerGroupId,
'approvalStatus -> fromApprovalStatus(batchChange.approvalStatus),
+ 'batchStatus -> fromBatchStatus(batchStatus),
'reviewerId -> batchChange.reviewerId,
'reviewComment -> batchChange.reviewComment,
'reviewTimestamp -> batchChange.reviewTimestamp,
@@ -385,7 +449,6 @@ class MySqlBatchChangeRepository
case Left(e) => throw e
}
}
-
PUT_SINGLE_CHANGE.batchByName(singleChangesParams: _*).apply()
batchChange
}
@@ -420,6 +483,18 @@ class MySqlBatchChangeRepository
case BatchChangeApprovalStatus.Cancelled => 5
}
+ def fromBatchStatus(typ: BatchChangeStatus): String =
+ typ match {
+ case BatchChangeStatus.Cancelled => "Cancelled"
+ case BatchChangeStatus.Complete => "Complete"
+ case BatchChangeStatus.Failed => "Failed"
+ case BatchChangeStatus.PartialFailure => "PartialFailure"
+ case BatchChangeStatus.PendingProcessing => "PendingProcessing"
+ case BatchChangeStatus.PendingReview => "PendingReview"
+ case BatchChangeStatus.Rejected => "Rejected"
+ case BatchChangeStatus.Scheduled => "Scheduled"
+ }
+
def toApprovalStatus(key: Option[Int]): BatchChangeApprovalStatus =
key match {
case Some(1) => BatchChangeApprovalStatus.AutoApproved
@@ -430,5 +505,19 @@ class MySqlBatchChangeRepository
case _ => BatchChangeApprovalStatus.AutoApproved
}
+ def toBatchChangeStatus(key: Option[String]): BatchChangeStatus = {
+ key match {
+ case Some("Cancelled") => BatchChangeStatus.Cancelled
+ case Some("Complete") => BatchChangeStatus.Complete
+ case Some("Failed") => BatchChangeStatus.Failed
+ case Some("PartialFailure") => BatchChangeStatus.PartialFailure
+ case Some("PendingProcessing") => BatchChangeStatus.PendingProcessing
+ case Some("PendingReview") => BatchChangeStatus.PendingReview
+ case Some("Rejected") => BatchChangeStatus.Rejected
+ case Some("Scheduled") => BatchChangeStatus.Scheduled
+ case _ => BatchChangeStatus.Complete
+ }
+ }
+
def toDateTime(ts: Timestamp): Instant = ts.toInstant
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlDataStoreProvider.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlDataStoreProvider.scala
index b3c6a8618..2a85a7fe6 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlDataStoreProvider.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlDataStoreProvider.scala
@@ -29,6 +29,7 @@ import vinyldns.core.repository._
import vinyldns.core.health.HealthCheck._
import vinyldns.mysql.{HikariCloser, MySqlConnectionConfig, MySqlDataSourceSettings}
import vinyldns.mysql.MySqlConnector._
+import java.io.{PrintWriter, StringWriter}
class MySqlDataStoreProvider extends DataStoreProvider {
@@ -99,7 +100,11 @@ class MySqlDataStoreProvider extends DataStoreProvider {
private def shutdown(): IO[Unit] =
IO(DBs.close())
- .handleError(e => logger.error(s"Exception occurred while shutting down", e))
+ .handleError{ e =>
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Exception occurred while shutting down. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
+ }
private final val HEALTH_CHECK =
sql"""
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupChangeRepository.scala
index 2773a7ca0..d8c200408 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupChangeRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupChangeRepository.scala
@@ -47,9 +47,9 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
sql"""
|SELECT data
| FROM group_change
- | WHERE group_id = {groupId} AND created_timestamp < {startFrom}
+ | WHERE group_id = {groupId}
| ORDER BY created_timestamp DESC
- | LIMIT {maxItems}
+ | LIMIT {maxItems} OFFSET {startFrom}
""".stripMargin
private final val LIST_GROUP_CHANGE_NO_START =
@@ -100,7 +100,7 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
def getGroupChanges(
groupId: String,
- startFrom: Option[String],
+ startFrom: Option[Int],
maxItems: Int
): IO[ListGroupChangesResults] =
monitor("repo.GroupChange.getGroupChanges") {
@@ -112,21 +112,25 @@ class MySqlGroupChangeRepository extends GroupChangeRepository with Monitored {
val query = startFrom match {
case Some(start) =>
LIST_GROUP_CHANGES_WITH_START
- .bindByName('groupId -> groupId, 'startFrom -> start, 'maxItems -> maxItems)
+ .bindByName('groupId -> groupId, 'startFrom -> start, 'maxItems -> (maxItems + 1))
case None =>
LIST_GROUP_CHANGE_NO_START
- .bindByName('groupId -> groupId, 'maxItems -> maxItems)
+ .bindByName('groupId -> groupId, 'maxItems -> (maxItems + 1))
}
val queryResult = query
.map(toGroupChange(1))
.list()
.apply()
- val nextId =
- if (queryResult.size < maxItems) None
- else queryResult.lastOption.map(_.created.toEpochMilli.toString)
+ val maxQueries = queryResult.take(maxItems)
+ val startValue = startFrom.getOrElse(0)
- ListGroupChangesResults(queryResult, nextId)
+ val nextId = queryResult match {
+ case _ if queryResult.size <= maxItems | queryResult.isEmpty => None
+ case _ => Some(startValue + maxItems)
+ }
+
+ ListGroupChangesResults(maxQueries, nextId)
}
}
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala
index 02311e6fd..caf1333a9 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlGroupRepository.scala
@@ -32,47 +32,47 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions
private final val PUT_GROUP =
sql"""
- |REPLACE INTO groups(id, name, data, description, created_timestamp, email)
+ |REPLACE INTO `groups`(id, name, data, description, created_timestamp, email)
| VALUES ({id}, {name}, {data}, {description}, {createdTimestamp}, {email})
""".stripMargin
private final val DELETE_GROUP =
sql"""
- |DELETE FROM groups
+ |DELETE FROM `groups`
| WHERE id = ?
""".stripMargin
private final val GET_GROUP_BY_ID =
sql"""
|SELECT data
- | FROM groups
+ | FROM `groups`
| WHERE id = ?
""".stripMargin
private final val GET_GROUP_BY_NAME =
sql"""
|SELECT data
- | FROM groups
+ | FROM `groups`
| WHERE name = ?
""".stripMargin
private final val GET_ALL_GROUPS =
sql"""
|SELECT data
- | FROM groups
+ | FROM `groups`
""".stripMargin
private val BASE_GET_GROUPS_BY_IDS =
"""
|SELECT data
- | FROM groups
+ | FROM `groups`
| WHERE id
""".stripMargin
private val BASE_GET_GROUPS_BY_NAMES =
"""
|SELECT data
- | FROM groups
+ | FROM `groups`
| WHERE name
""".stripMargin
@@ -187,7 +187,7 @@ class MySqlGroupRepository extends GroupRepository with GroupProtobufConversions
monitor("repo.Group.getGroupByName") {
IO {
logger.debug(s"Getting groups with name: $nameFilter")
- val initialQuery = "SELECT data FROM groups WHERE name"
+ val initialQuery = "SELECT data FROM `groups` WHERE name"
val sb = new StringBuilder
sb.append(initialQuery)
val groupsLike = if (nameFilter.contains('*')) {
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordChangeRepository.scala
index 18f1902dc..2e7f6d386 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordChangeRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordChangeRepository.scala
@@ -19,9 +19,11 @@ package vinyldns.mysql.repository
import cats.effect._
import scalikejdbc._
import vinyldns.core.domain.record.RecordSetChangeType.RecordSetChangeType
+import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.core.domain.record._
import vinyldns.core.protobuf.ProtobufConversions
import vinyldns.core.route.Monitored
+import vinyldns.mysql.repository.MySqlRecordSetRepository.{fromRecordType, toFQDN}
import vinyldns.proto.VinylDNSProto
class MySqlRecordChangeRepository
@@ -33,20 +35,45 @@ class MySqlRecordChangeRepository
private val LIST_CHANGES_WITH_START =
sql"""
|SELECT data
- | FROM record_change
+ | FROM record_change
| WHERE zone_id = {zoneId}
- | AND created < {created}
- | ORDER BY created DESC
- | LIMIT {limit}
+ | ORDER BY created DESC
+ | LIMIT {limit} OFFSET {startFrom}
+ """.stripMargin
+
+ private val LIST_CHANGES_WITH_START_FQDN_TYPE =
+ sql"""
+ |SELECT data
+ | FROM record_change
+ | WHERE fqdn = {fqdn} AND record_type = {type}
+ | ORDER BY created DESC
+ | LIMIT {limit} OFFSET {startFrom}
+ """.stripMargin
+
+ private val LIST_CHANGES_WITHOUT_START_FQDN_TYPE =
+ sql"""
+ |SELECT data
+ | FROM record_change
+ | WHERE fqdn = {fqdn} AND record_type = {type}
+ | ORDER BY created DESC
+ | LIMIT {limit}
+ """.stripMargin
+
+ private val LIST_RECORD_CHANGES =
+ sql"""
+ |SELECT data
+ | FROM record_change
+ | WHERE zone_id = {zoneId}
+ | ORDER BY created DESC
""".stripMargin
private val LIST_CHANGES_NO_START =
sql"""
|SELECT data
- | FROM record_change
+ | FROM record_change
| WHERE zone_id = {zoneId}
- | ORDER BY created DESC
- | LIMIT {limit}
+ | ORDER BY created DESC
+ | LIMIT {limit}
""".stripMargin
private val GET_CHANGE =
@@ -57,7 +84,7 @@ class MySqlRecordChangeRepository
""".stripMargin
private val INSERT_CHANGES =
- sql"INSERT IGNORE INTO record_change (id, zone_id, created, type, data) VALUES (?, ?, ?, ?, ?)"
+ sql"INSERT IGNORE INTO record_change (id, zone_id, created, type, fqdn, record_type, data) VALUES (?, ?, ?, ?, ?, ?, ?)"
/**
* We have the same issue with changes as record sets, namely we may have to save millions of them
@@ -76,7 +103,9 @@ class MySqlRecordChangeRepository
change.zoneId,
change.created.toEpochMilli,
fromChangeType(change.changeType),
- toPB(change).toByteArray
+ toFQDN(change.zone.name, change.recordSet.name),
+ fromRecordType(change.recordSet.typ),
+ toPB(change).toByteArray,
)
}
}
@@ -88,34 +117,52 @@ class MySqlRecordChangeRepository
}
def listRecordSetChanges(
- zoneId: String,
- startFrom: Option[String],
- maxItems: Int
+ zoneId: Option[String],
+ startFrom: Option[Int],
+ maxItems: Int,
+ fqdn: Option[String],
+ recordType: Option[RecordType]
): IO[ListRecordSetChangesResults] =
monitor("repo.RecordChange.listRecordSetChanges") {
IO {
DB.readOnly { implicit s =>
- val changes = startFrom match {
- case Some(start) =>
- LIST_CHANGES_WITH_START
- .bindByName('zoneId -> zoneId, 'created -> start.toLong, 'limit -> maxItems)
- .map(toRecordSetChange)
- .list()
- .apply()
- case None =>
- LIST_CHANGES_NO_START
- .bindByName('zoneId -> zoneId, 'limit -> maxItems)
- .map(toRecordSetChange)
- .list()
- .apply()
+ val changes = if(startFrom.isDefined && fqdn.isDefined && recordType.isDefined){
+ LIST_CHANGES_WITH_START_FQDN_TYPE
+ .bindByName('fqdn -> fqdn.get, 'type -> fromRecordType(recordType.get), 'startFrom -> startFrom.get, 'limit -> (maxItems + 1))
+ .map(toRecordSetChange)
+ .list()
+ .apply()
+ } else if(fqdn.isDefined && recordType.isDefined){
+ LIST_CHANGES_WITHOUT_START_FQDN_TYPE
+ .bindByName('fqdn -> fqdn.get, 'type -> fromRecordType(recordType.get), 'limit -> (maxItems + 1))
+ .map(toRecordSetChange)
+ .list()
+ .apply()
+ } else if(startFrom.isDefined){
+ LIST_CHANGES_WITH_START
+ .bindByName('zoneId -> zoneId.get, 'startFrom -> startFrom.get, 'limit -> (maxItems + 1))
+ .map(toRecordSetChange)
+ .list()
+ .apply()
+ } else {
+ LIST_CHANGES_NO_START
+ .bindByName('zoneId -> zoneId.get, 'limit -> (maxItems + 1))
+ .map(toRecordSetChange)
+ .list()
+ .apply()
}
- val nextId =
- if (changes.size < maxItems) None
- else changes.lastOption.map(_.created.toEpochMilli.toString)
+ val maxQueries = changes.take(maxItems)
+ val startValue = startFrom.getOrElse(0)
+
+ // earlier maxItems was incremented, if the (maxItems + 1) size is not reached then pages are exhausted
+ val nextId = changes match {
+ case _ if changes.size <= maxItems | changes.isEmpty => None
+ case _ => Some(startValue + maxItems)
+ }
ListRecordSetChangesResults(
- changes,
+ maxQueries,
nextId,
startFrom,
maxItems
@@ -124,6 +171,22 @@ class MySqlRecordChangeRepository
}
}
+ def listFailedRecordSetChanges(zoneId: Option[String], maxItems: Int, startFrom: Int): IO[ListFailedRecordSetChangesResults] =
+ monitor("repo.RecordChange.listFailedRecordSetChanges") {
+ IO {
+ DB.readOnly { implicit s =>
+ val queryResult = LIST_RECORD_CHANGES
+ .bindByName('zoneId -> zoneId.get)
+ .map(toRecordSetChange)
+ .list()
+ .apply()
+ val failedRecordSetChanges = queryResult.filter(rc => rc.status == RecordSetChangeStatus.Failed).drop(startFrom).take(maxItems)
+ val nextId = if (failedRecordSetChanges.size < maxItems) 0 else startFrom + maxItems + 1
+ ListFailedRecordSetChangesResults(failedRecordSetChanges,nextId,startFrom,maxItems)
+ }
+ }
+ }
+
def getRecordSetChange(zoneId: String, changeId: String): IO[Option[RecordSetChange]] =
monitor("repo.RecordChange.listRecordSetChanges") {
IO {
@@ -139,6 +202,7 @@ object MySqlRecordChangeRepository extends ProtobufConversions {
case RecordSetChangeType.Create => 1
case RecordSetChangeType.Delete => 2
case RecordSetChangeType.Update => 3
+ case RecordSetChangeType.Sync => 4
}
def toRecordSetChange(ws: WrappedResultSet): RecordSetChange =
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetCacheRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetCacheRepository.scala
index 1a8e30ece..9cf67808a 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetCacheRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetCacheRepository.scala
@@ -28,8 +28,8 @@ import vinyldns.core.domain.record.NameSort.{ASC, NameSort}
import vinyldns.core.domain.record.RecordType.RecordType
import vinyldns.mysql.repository.MySqlRecordSetRepository.{PagingKey, fromRecordType, toFQDN}
import vinyldns.proto.VinylDNSProto
-
-import scala.util.{Try, Success, Failure}
+import java.io.{PrintWriter, StringWriter}
+import scala.util.{Failure, Success, Try}
class MySqlRecordSetCacheRepository
extends RecordSetCacheRepository
@@ -139,7 +139,9 @@ class MySqlRecordSetCacheRepository
}
logger.info(s"Deleted $numDeleted records from zone $zoneName (zone id: $zone_id)")
}.handleErrorWith { error =>
- logger.error(s"Failed deleting records from zone $zoneName (zone id: $zone_id)", error)
+ val errorMessage = new StringWriter
+ error.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Failed deleting records from zone $zoneName (zone id: $zone_id). Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
IO.raiseError(error)
}
}
@@ -394,7 +396,8 @@ class MySqlRecordSetCacheRepository
maxItems = maxItems,
recordNameFilter = recordNameFilter,
recordTypeFilter = recordTypeFilter,
- nameSort = nameSort)
+ nameSort = nameSort,
+ recordTypeSort = RecordTypeSort.NONE)
}
}
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetRepository.scala
index adf811525..ec666f87a 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlRecordSetRepository.scala
@@ -20,13 +20,14 @@ import cats.effect._
import cats.implicits._
import org.slf4j.LoggerFactory
import scalikejdbc._
-import vinyldns.core.domain.record.NameSort.{ASC, NameSort}
-import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.NameSort.NameSort
import vinyldns.core.domain.record._
+import vinyldns.core.domain.record.RecordType.RecordType
+import vinyldns.core.domain.record.RecordTypeSort.RecordTypeSort
import vinyldns.core.protobuf.ProtobufConversions
import vinyldns.core.route.Monitored
import vinyldns.proto.VinylDNSProto
-
+import java.io.{PrintWriter, StringWriter}
import scala.util.Try
class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
@@ -176,7 +177,8 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
recordNameFilter: Option[String],
recordTypeFilter: Option[Set[RecordType]],
recordOwnerGroupFilter: Option[String],
- nameSort: NameSort
+ nameSort: NameSort,
+ recordTypeSort: RecordTypeSort,
): IO[ListRecordSetResults] =
monitor("repo.RecordSet.listRecordSets") {
IO {
@@ -226,11 +228,16 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
val opts =
(zoneAndNameFilters ++ sortBy ++ typeFilter ++ ownerGroupFilter).toList
- val qualifiers = if (nameSort == ASC) {
- sqls"ORDER BY fqdn ASC, type ASC "
+ val nameSortQualifiers = nameSort match {
+ case NameSort.ASC => sqls"ORDER BY fqdn ASC, type ASC "
+ case NameSort.DESC => sqls"ORDER BY fqdn DESC, type ASC "
}
- else {
- sqls"ORDER BY fqdn DESC, type ASC "
+
+ val recordTypeSortQualifiers = recordTypeSort match {
+ case RecordTypeSort.ASC => sqls"ORDER BY type ASC"
+ case RecordTypeSort.DESC => sqls"ORDER BY type DESC"
+ case RecordTypeSort.NONE => nameSortQualifiers
+
}
val recordLimit = maxPlusOne match {
@@ -238,7 +245,7 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
case None => sqls""
}
- val finalQualifiers = qualifiers.append(recordLimit)
+ val finalQualifiers = recordTypeSortQualifiers.append(recordLimit)
// construct query
val initialQuery = sqls"SELECT data, fqdn FROM recordset "
@@ -278,7 +285,8 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
maxItems = maxItems,
recordNameFilter = recordNameFilter,
recordTypeFilter = recordTypeFilter,
- nameSort = nameSort
+ nameSort = nameSort,
+ recordTypeSort = recordTypeSort
)
}
}
@@ -380,7 +388,9 @@ class MySqlRecordSetRepository extends RecordSetRepository with Monitored {
}
logger.debug(s"Deleted $numDeleted records from zone $zoneName (zone id: $zoneId)")
}.handleErrorWith { error =>
- logger.error(s"Failed deleting records from zone $zoneName (zone id: $zoneId)", error)
+ val errorMessage = new StringWriter
+ error.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Failed deleting records from zone $zoneName (zone id: $zoneId). Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
IO.raiseError(error)
}
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala
index 2f9c0368b..f9deec42b 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneChangeRepository.scala
@@ -17,10 +17,13 @@
package vinyldns.mysql.repository
import cats.effect.IO
+
import java.time.Instant
import java.time.temporal.ChronoUnit
import org.slf4j.LoggerFactory
import scalikejdbc._
+import vinyldns.core.domain.auth.AuthPrincipal
+import vinyldns.core.domain.membership.User
import vinyldns.core.domain.zone._
import vinyldns.core.protobuf._
import vinyldns.core.route.Monitored
@@ -32,12 +35,32 @@ class MySqlZoneChangeRepository
with Monitored {
private final val logger = LoggerFactory.getLogger(classOf[MySqlZoneChangeRepository])
+ private final val MAX_ACCESSORS = 30
+
private final val PUT_ZONE_CHANGE =
sql"""
- |REPLACE INTO zone_change (change_id, zone_id, data, created_timestamp)
- | VALUES ({change_id}, {zone_id}, {data}, {created_timestamp})
+ |REPLACE INTO zone_change (change_id, zone_id, data, created_timestamp, zone_name, zone_status)
+ | VALUES ({change_id}, {zone_id}, {data}, {created_timestamp},{zone_name}, {zone_status})
""".stripMargin
+ private final val BASE_ZONE_CHANGE_SEARCH_SQL =
+ """
+ |SELECT zc.data
+ | FROM zone_change zc
+ """.stripMargin
+
+ private final val BASE_ZONE_NAME_COUNT_SQL =
+ """
+ |SELECT COUNT(z.name)
+ | FROM zone z
+ """.stripMargin
+
+ private final val BASE_ZONE_NAME_SEARCH_SQL =
+ """
+ |SELECT z.name
+ | FROM zone z
+ """.stripMargin
+
private final val LIST_ZONES_CHANGES =
sql"""
|SELECT zc.data
@@ -47,17 +70,27 @@ class MySqlZoneChangeRepository
| LIMIT {maxItems}
""".stripMargin
+ private final val LIST_ZONES_CHANGES_DATA =
+ sql"""
+ |SELECT zc.data
+ | FROM zone_change zc
+ | JOIN zone z ON z.id = zc.zone_id
+ | ORDER BY zc.created_timestamp DESC
+ """.stripMargin
+
override def save(zoneChange: ZoneChange): IO[ZoneChange] =
monitor("repo.ZoneChange.save") {
IO {
- logger.debug(s"Saving zone change ${zoneChange.id}")
+ logger.debug(s"Saving zone change '${zoneChange.id}' for zone '${zoneChange.zone.name}'")
DB.localTx { implicit s =>
PUT_ZONE_CHANGE
.bindByName(
'change_id -> zoneChange.id,
'zone_id -> zoneChange.zoneId,
'data -> toPB(zoneChange).toByteArray,
- 'created_timestamp -> zoneChange.created.toEpochMilli
+ 'created_timestamp -> zoneChange.created.toEpochMilli,
+ 'zone_name -> zoneChange.zone.name,
+ 'zone_status -> zoneChange.zone.status.toString
)
.update()
.apply()
@@ -102,6 +135,132 @@ class MySqlZoneChangeRepository
}
}
+ private def withAccessors(
+ user: User,
+ groupIds: Seq[String],
+ ignoreAccessZones: Boolean
+ ): (String, Seq[Any]) =
+ // Super users do not need to join across to check zone access as they have access to all of the zones
+ if (ignoreAccessZones || user.isSuper || user.isSupport) {
+ (BASE_ZONE_CHANGE_SEARCH_SQL, Seq.empty)
+ } else {
+ // User is not super or support,
+ // let's join across to the zone access table so we return only zones a user has access to
+ val accessors = buildZoneSearchAccessorList(user, groupIds)
+ val questionMarks = List.fill(accessors.size)("?").mkString(",")
+ val withAccessorCheck = BASE_ZONE_CHANGE_SEARCH_SQL +
+ s"""
+ | JOIN zone_access za ON zc.zone_id = za.zone_id
+ | AND za.accessor_id IN ($questionMarks)
+ """.stripMargin
+ (withAccessorCheck, accessors)
+ }
+
+ /* Limit the accessors so that we don't have boundless parameterized queries */
+ private def buildZoneSearchAccessorList(user: User, groupIds: Seq[String]): Seq[String] = {
+ val allAccessors = user.id +: groupIds
+
+ if (allAccessors.length > MAX_ACCESSORS) {
+ logger.warn(
+ s"User ${user.userName} with id ${user.id} is in more than $MAX_ACCESSORS groups, no all zones maybe returned!"
+ )
+ }
+
+ // Take the top 30 accessors, but add "EVERYONE" to the list so that we include zones that have everyone access
+ allAccessors.take(MAX_ACCESSORS) :+ "EVERYONE"
+ }
+
+ def listDeletedZones(
+ authPrincipal: AuthPrincipal,
+ zoneNameFilter: Option[String] = None,
+ startFrom: Option[String] = None,
+ maxItems: Int = 100,
+ ignoreAccess: Boolean = false
+ ): IO[ListDeletedZonesChangeResults] =
+ monitor("repo.ZoneChange.listDeletedZoneInZoneChanges") {
+ IO {
+ DB.readOnly { implicit s =>
+ val (withAccessorCheck, accessors) =
+ withAccessors(authPrincipal.signedInUser, authPrincipal.memberGroupIds, ignoreAccess)
+ val sb = new StringBuilder
+ sb.append(withAccessorCheck)
+
+ val zoneResults: Int =
+ SQL(BASE_ZONE_NAME_COUNT_SQL)
+ .map(_.int(1))
+ .single()
+ .apply()
+ .getOrElse(0)
+
+ sb.append(s" WHERE ")
+
+ if (zoneResults != 0) sb.append(s" zc.zone_name NOT IN ($BASE_ZONE_NAME_SEARCH_SQL) AND ")
+
+ sb.append(s" zc.zone_status = 'Deleted' ")
+
+ val filters = if (zoneNameFilter.isDefined && zoneNameFilter.get.contains("*"))
+ zoneNameFilter.map(flt => s"zc.zone_name LIKE '${flt.replace('*', '%')}'")
+ else zoneNameFilter.map(flt => s"zc.zone_name LIKE '${flt.concat("%")}'")
+
+ if(zoneNameFilter.isDefined)
+ sb.append(s" AND ")
+
+ sb.append(filters.mkString)
+
+ val resultOrdering = s"""| GROUP BY zc.zone_name
+ | ORDER BY zc.created_timestamp DESC
+ """.stripMargin
+
+ sb.append(resultOrdering)
+
+ val query = sb.toString
+
+ val deletedZoneResults: List[ZoneChange] =
+ SQL(query)
+ .bind(accessors: _*)
+ .map(extractZoneChange(1))
+ .list()
+ .apply()
+
+ val deletedZonesWithStartFrom: List[ZoneChange] = startFrom match {
+ case Some(zoneId) => deletedZoneResults.dropWhile(_.zone.id != zoneId)
+ case None => deletedZoneResults
+ }
+
+ val deletedZonesWithMaxItems = deletedZonesWithStartFrom.take(maxItems + 1)
+
+ val (newResults, nextId) =
+ if (deletedZonesWithMaxItems.size > maxItems)
+ (deletedZonesWithMaxItems.dropRight(1), deletedZonesWithMaxItems.lastOption.map(_.zone.id))
+ else (deletedZonesWithMaxItems, None)
+
+ ListDeletedZonesChangeResults(
+ zoneDeleted = newResults,
+ nextId = nextId,
+ startFrom = startFrom,
+ maxItems = maxItems,
+ zoneChangeFilter = zoneNameFilter,
+ ignoreAccess = ignoreAccess
+ )
+ }}}
+
+ def listFailedZoneChanges(maxItems: Int, startFrom: Int): IO[ListFailedZoneChangesResults] =
+ monitor("repo.ZoneChange.listFailedZoneChanges") {
+ IO {
+ DB.readOnly { implicit s =>
+ val queryResult = LIST_ZONES_CHANGES_DATA
+ .map(extractZoneChange(1))
+ .list()
+ .apply()
+
+ val failedZoneChanges = queryResult.filter(zc => zc.status == ZoneChangeStatus.Failed).drop(startFrom).take(maxItems)
+ val nextId = if (failedZoneChanges.size < maxItems) 0 else startFrom + maxItems
+
+ ListFailedZoneChangesResults(failedZoneChanges,nextId,startFrom,maxItems)
+ }
+ }
+ }
+
private def extractZoneChange(colIndex: Int): WrappedResultSet => ZoneChange = res => {
fromPB(VinylDNSProto.ZoneChange.parseFrom(res.bytes(colIndex)))
}
diff --git a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala
index d08b90309..6a15a9853 100644
--- a/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala
+++ b/modules/mysql/src/main/scala/vinyldns/mysql/repository/MySqlZoneRepository.scala
@@ -48,10 +48,11 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
*/
private final val PUT_ZONE =
sql"""
- |INSERT INTO zone(id, name, admin_group_id, data)
- | VALUES ({id}, {name}, {adminGroupId}, {data}) ON DUPLICATE KEY
+ |INSERT INTO zone(id, name, admin_group_id, zone_sync_schedule, data)
+ | VALUES ({id}, {name}, {adminGroupId}, {recurrenceSchedule}, {data}) ON DUPLICATE KEY
| UPDATE name=VALUES(name),
| admin_group_id=VALUES(admin_group_id),
+ | zone_sync_schedule=VALUES(zone_sync_schedule),
| data=VALUES(data);
""".stripMargin
@@ -70,10 +71,17 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
*/
private final val PUT_ZONE_ACCESS =
sql"""
- |REPLACE INTO zone_access(accessor_id, zone_id)
- | VALUES ({accessorId}, {zoneId})
+ |REPLACE INTO zone_access(accessor_id, zone_id, zone_status)
+ | VALUES ({accessorId}, {zoneId}, {zoneStatus})
""".stripMargin
+ private final val UPDATE_ZONE_ACCESS =
+ sql"""
+ |UPDATE zone_access
+ | SET zone_status = {zoneStatus}
+ | WHERE zone_id = {zoneId}
+ """.stripMargin
+
private final val DELETE_ZONE_ACCESS =
sql"""
|DELETE
@@ -116,11 +124,18 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
| FROM zone
""".stripMargin
+ private final val BASE_GET_ALL_ZONES_SQL =
+ """
+ |SELECT data
+ | FROM zone
+ | WHERE zone_sync_schedule IS NOT NULL
+ """.stripMargin
+
private final val GET_ZONE_ACCESS_BY_ADMIN_GROUP_ID =
sql"""
|SELECT zone_id
- | FROM zone_access z
- | WHERE z.accessor_id = (?)
+ | FROM zone_access za
+ | WHERE za.accessor_id = (?) AND za.zone_status <> 'Deleted'
| LIMIT 1
""".stripMargin
@@ -207,6 +222,19 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
}
}
+ def getAllZonesWithSyncSchedule: IO[Set[Zone]] =
+ monitor("repo.ZoneJDBC.getAllZonesWithSyncSchedule") {
+ IO {
+ DB.readOnly { implicit s =>
+ SQL(
+ BASE_GET_ALL_ZONES_SQL
+ ).map(extractZone(1))
+ .list()
+ .apply()
+ }.toSet
+ }
+ }
+
def getZonesByFilters(zoneNames: Set[String]): IO[Set[Zone]] =
if (zoneNames.isEmpty) {
IO.pure(Set())
@@ -242,7 +270,8 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
startFrom: Option[String] = None,
maxItems: Int = 100,
adminGroupIds: Set[String],
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults] =
monitor("repo.ZoneJDBC.listZonesByAdminGroupIds") {
IO {
@@ -252,6 +281,11 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
val sb = new StringBuilder
sb.append(withAccessorCheck)
+ val noReverseRegex =
+ if (!includeReverse)
+ """(in-addr\.arpa\.)|(ip6\.arpa\.)$"""
+ else None
+
if(adminGroupIds.nonEmpty) {
val groupIds = adminGroupIds.map(x => "'" + x + "'").mkString(",")
sb.append(s" WHERE admin_group_id IN ($groupIds) ")
@@ -259,6 +293,16 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
sb.append(s" WHERE admin_group_id IN ('') ")
}
+ if (!includeReverse) {
+ sb.append(" AND ")
+ sb.append(s"z.name NOT RLIKE '$noReverseRegex'")
+ }
+
+ if(startFrom.isDefined){
+ sb.append(" AND ")
+ sb.append(s"z.name > '${startFrom.get}'")
+ }
+
sb.append(s" GROUP BY z.name ")
sb.append(s" LIMIT ${maxItems + 1}")
@@ -283,6 +327,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
maxItems = maxItems,
zonesFilter = None,
ignoreAccess = ignoreAccess,
+ includeReverse = includeReverse
)
}
}
@@ -302,7 +347,8 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
zoneNameFilter: Option[String] = None,
startFrom: Option[String] = None,
maxItems: Int = 100,
- ignoreAccess: Boolean = false
+ ignoreAccess: Boolean = false,
+ includeReverse: Boolean = true
): IO[ListZonesResults] =
monitor("repo.ZoneJDBC.listZones") {
IO {
@@ -312,16 +358,39 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
val sb = new StringBuilder
sb.append(withAccessorCheck)
- val filters = List(
- zoneNameFilter.map(flt => s"z.name LIKE '${ensureTrailingDot(flt.replace('*', '%'))}'"),
- startFrom.map(os => s"z.name > '$os'")
- ).flatten
+ val noReverseRegex =
+ if (!includeReverse)
+ """(in-addr\.arpa\.)|(ip6\.arpa\.)$"""
+ else None
+
+ val filters = if (zoneNameFilter.isDefined && (zoneNameFilter.get.takeRight(1) == "." || zoneNameFilter.get.contains("*"))) {
+ List(
+ zoneNameFilter.map(flt => s"z.name LIKE '${ensureTrailingDot(flt.replace('*', '%'))}'"),
+ startFrom.map(os => s"z.name > '$os'")
+ ).flatten
+ } else {
+ List(
+ zoneNameFilter.map(flt => s"z.name LIKE '${flt.concat("%")}'"),
+ startFrom.map(os => s"z.name > '$os'")
+ ).flatten
+ }
if (filters.nonEmpty) {
sb.append(" WHERE ")
sb.append(filters.mkString(" AND "))
}
+ if (!includeReverse) {
+ if (filters.nonEmpty) {
+ sb.append(" AND ")
+ sb.append(s"z.name NOT RLIKE '$noReverseRegex'")
+ }
+ else {
+ sb.append(" WHERE ")
+ sb.append(s"z.name NOT RLIKE '$noReverseRegex'")
+ }
+ }
+
sb.append(s" GROUP BY z.name ")
sb.append(s" LIMIT ${maxItems + 1}")
@@ -344,7 +413,8 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
startFrom = startFrom,
maxItems = maxItems,
zonesFilter = zoneNameFilter,
- ignoreAccess = ignoreAccess
+ ignoreAccess = ignoreAccess,
+ includeReverse = includeReverse
)
}
}
@@ -414,6 +484,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
'id -> zone.id,
'name -> zone.name,
'adminGroupId -> zone.adminGroupId,
+ 'recurrenceSchedule -> zone.recurrenceSchedule,
'data -> toPB(zone).toByteArray
): _*
)
@@ -439,10 +510,10 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
val sqlParameters: Seq[Seq[(Symbol, Any)]] =
zone.acl.rules.toSeq
.map(r => r.userId.orElse(r.groupId).getOrElse("EVERYONE")) // if the user and group are empty, assert everyone
- .map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id))
+ .map(userOrGroupId => Seq('accessorId -> userOrGroupId, 'zoneId -> zone.id, 'zoneStatus -> zone.status.toString))
// we MUST make sure that we put the admin group id as an accessor to this zone
- val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id)
+ val allAccessors = sqlParameters :+ Seq('accessorId -> zone.adminGroupId, 'zoneId -> zone.id,'zoneStatus -> zone.status.toString)
// make sure that we do a distinct, so that we don't generate unnecessary inserts
PUT_ZONE_ACCESS.batchByName(allAccessors.distinct: _*).apply()
@@ -454,6 +525,12 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
zone
}
+ private def updateZoneAccess(zone: Zone)(implicit session: DBSession): Zone = {
+ UPDATE_ZONE_ACCESS.bindByName(
+ 'zoneStatus ->zone.status.toString, 'zoneId ->zone.id).update().apply()
+ zone
+ }
+
private def deleteZoneAccess(zone: Zone)(implicit session: DBSession): Zone = {
DELETE_ZONE_ACCESS.bind(zone.id).update().apply()
zone
@@ -468,6 +545,7 @@ class MySqlZoneRepository extends ZoneRepository with ProtobufConversions with M
IO {
DB.localTx { implicit s =>
deleteZone(zone)
+ updateZoneAccess(zone)
}
}
}
diff --git a/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlDataStoreProviderSpec.scala b/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlDataStoreProviderSpec.scala
index 7651f0e5f..bff6c6deb 100644
--- a/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlDataStoreProviderSpec.scala
+++ b/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlDataStoreProviderSpec.scala
@@ -49,7 +49,7 @@ class MySqlDataStoreProviderSpec extends AnyWordSpec with Matchers {
| migration-url = "test-url"
| maximum-pool-size = 20
| connection-timeout-millis = 1000
- | max-life-time = 600000
+ | max-lifetime = 600000
| }
|
| repositories {
diff --git a/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlZoneRepositorySpec.scala b/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlZoneRepositorySpec.scala
index 8d2f5f6ff..190f786a1 100644
--- a/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlZoneRepositorySpec.scala
+++ b/modules/mysql/src/test/scala/vinyldns/mysql/repository/MySqlZoneRepositorySpec.scala
@@ -169,5 +169,33 @@ class MySqlZoneRepositorySpec
result shouldEqual Right(zoneInput)
}
}
+ "MySqlZoneRepository.getAllZonesWithSyncSchedule" should {
+ "get zones which have zone sync scheduled" in {
+ // save a zone with recurrence schedule
+ val zoneInput1 = Zone("ok.", "test@test.com", ZoneStatus.Active, recurrenceSchedule = Some("0/5 0 0 ? * * *"))
+ doReturn(IO.pure(Right(zoneInput1)))
+ .when(repo)
+ .saveTx(zoneInput1)
+ val zone1 = repo.save(zoneInput1).unsafeRunSync()
+ verify(repo).save(zoneInput1)
+ zone1 shouldEqual Right(zoneInput1)
+
+ // save a zone without recurrence schedule
+ val zoneInput2 = Zone("dummy.", "test@test.com", ZoneStatus.Active)
+ doReturn(IO.pure(Right(zoneInput2)))
+ .when(repo)
+ .saveTx(zoneInput2)
+ val zone2 = repo.save(zoneInput2).unsafeRunSync()
+ verify(repo).save(zoneInput2)
+ zone2 shouldEqual Right(zoneInput2)
+
+ // Only get zones with schedule
+ doReturn(IO.pure(Set(zoneInput1)))
+ .when(repo)
+ .getAllZonesWithSyncSchedule
+ val result = repo.getAllZonesWithSyncSchedule.unsafeRunSync()
+ result shouldEqual Set(zoneInput1)
+ }
+ }
}
diff --git a/modules/portal/Gruntfile.js b/modules/portal/Gruntfile.js
index 7a3123934..d04d031ca 100644
--- a/modules/portal/Gruntfile.js
+++ b/modules/portal/Gruntfile.js
@@ -35,10 +35,12 @@ module.exports = function(grunt) {
{expand: true, flatten: true, src: ['node_modules/jquery/dist/jquery.min.js'], dest: 'public/js'},
{expand: true, flatten: true, src: ['node_modules/moment/min/moment.min.js'], dest: 'public/js'},
{expand: true, flatten: true, src: ['node_modules/jquery-ui-dist/jquery-ui.js'], dest: 'public/js'},
+ {expand: true, flatten: true, src: ['node_modules/angular-cron-jobs/dist/angular-cron-jobs.min.js'], dest: 'public/js'},
{expand: true, flatten: true, src: ['node_modules/bootstrap/dist/css/bootstrap.min.css'], dest: 'public/css'},
{expand: true, flatten: true, src: ['node_modules/font-awesome/css/font-awesome.min.css'], dest: 'public/css'},
{expand: true, flatten: true, src: ['node_modules/jquery-ui-dist/jquery-ui.css'], dest: 'public/css'},
+ {expand: true, flatten: true, src: ['node_modules/angular-cron-jobs/dist/angular-cron-jobs.min.css'], dest: 'public/css'},
// We're picking just the resources we need from the gentelella UI framework and temporarily storing them in mapped/ui/
{expand: true, flatten: true, cwd: 'node_modules/gentelella', dest: 'mapped/ui', src: '**/jquery.{smartWizard,dataTables.min,mousewheel.min}.js'},
diff --git a/modules/portal/app/controllers/FrontendController.scala b/modules/portal/app/controllers/FrontendController.scala
index 53c1ff17b..d51bf4029 100644
--- a/modules/portal/app/controllers/FrontendController.scala
+++ b/modules/portal/app/controllers/FrontendController.scala
@@ -71,11 +71,13 @@ class FrontendController @Inject() (
}
def viewZone(zoneId: String): Action[AnyContent] = userAction.async { implicit request =>
- Future(Ok(views.html.zones.zoneDetail(request.user.userName, zoneId)))
+ val canReview = request.user.isSuper || request.user.isSupport
+ Future(Ok(views.html.zones.zoneDetail(request.user.userName, canReview, zoneId)))
}
def viewRecordSets(): Action[AnyContent] = userAction.async { implicit request =>
- Future(Ok(views.html.recordsets.recordSets(request.user.userName)))
+ val canReview = request.user.isSuper || request.user.isSupport
+ Future(Ok(views.html.recordsets.recordSets(request.user.userName, canReview)))
}
def viewAllBatchChanges(): Action[AnyContent] = userAction.async { implicit request =>
diff --git a/modules/portal/app/controllers/LdapAuthenticator.scala b/modules/portal/app/controllers/LdapAuthenticator.scala
index 2b445cd67..e5c1fcddd 100644
--- a/modules/portal/app/controllers/LdapAuthenticator.scala
+++ b/modules/portal/app/controllers/LdapAuthenticator.scala
@@ -27,7 +27,7 @@ import javax.naming.directory._
import org.slf4j.LoggerFactory
import vinyldns.core.domain.membership.User
import vinyldns.core.health.HealthCheck._
-
+import java.io.{PrintWriter, StringWriter}
import scala.collection.JavaConverters._
import scala.util.{Failure, Success, Try}
@@ -134,9 +134,10 @@ object LdapAuthenticator {
else Left(UserDoesNotExistException(s"[$lookupUserName] LDAP entity does not exist"))
} catch {
case unexpectedError: Throwable =>
+ val errorMessage = new StringWriter
+ unexpectedError.printStackTrace(new PrintWriter(errorMessage))
logger.error(
- s"LDAP Unexpected Error searching for user; userName='$lookupUserName'",
- unexpectedError
+ s"LDAP Unexpected Error searching for user; userName='$lookupUserName'. Error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}"
)
Left(LdapServiceException(unexpectedError.getMessage))
} finally {
diff --git a/modules/portal/app/controllers/OidcAuthenticator.scala b/modules/portal/app/controllers/OidcAuthenticator.scala
index 4fa8ed4af..08e99cfc7 100644
--- a/modules/portal/app/controllers/OidcAuthenticator.scala
+++ b/modules/portal/app/controllers/OidcAuthenticator.scala
@@ -46,6 +46,7 @@ import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success, Try}
import scala.collection.JavaConverters._
import pureconfig.ConfigSource
+import java.io.{PrintWriter, StringWriter}
object OidcAuthenticator {
final case class OidcConfig(
@@ -186,7 +187,9 @@ class OidcAuthenticator @Inject() (wsClient: WSClient, configuration: Configurat
val claimsSet = Try(JWTClaimsSet.parse(jwtClaimsSetString)) match {
case Success(s) => Some(s)
case Failure(e) =>
- logger.error(s"oidc session token parse error: ${e.getMessage}")
+ val errorMessage = new StringWriter
+ e.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"oidc session token parse error: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
None
}
@@ -260,7 +263,9 @@ class OidcAuthenticator @Inject() (wsClient: WSClient, configuration: Configurat
Either
.fromTry(Try(t))
.leftMap { err =>
- logger.error(s"Unexpected error in OIDC flow: ${err.getMessage}")
+ val errorMessage = new StringWriter
+ err.printStackTrace(new PrintWriter(errorMessage))
+ logger.error(s"Unexpected error in OIDC flow: ${errorMessage.toString.replaceAll("\n",";").replaceAll("\t"," ")}")
ErrorResponse(500, err.getMessage)
}
}
diff --git a/modules/portal/app/controllers/VinylDNS.scala b/modules/portal/app/controllers/VinylDNS.scala
index f6d3852e9..c9affbf85 100644
--- a/modules/portal/app/controllers/VinylDNS.scala
+++ b/modules/portal/app/controllers/VinylDNS.scala
@@ -35,6 +35,7 @@ import play.api.libs.json._
import play.api.libs.ws.{BodyWritable, InMemoryBody, WSClient}
import play.api.mvc._
import vinyldns.core.crypto.CryptoAlgebra
+import vinyldns.core.domain.Encrypted
import vinyldns.core.domain.membership.LockStatus.LockStatus
import vinyldns.core.domain.membership.{LockStatus, User}
import vinyldns.core.logging.RequestTracing
@@ -292,6 +293,24 @@ class VinylDNS @Inject() (
})
}
+ def getValidEmailDomains(): Action[AnyContent] = userAction.async { implicit request =>
+ val vinyldnsRequest =
+ VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"groups/valid/domains")
+ executeRequest(vinyldnsRequest, request.user).map(response => {
+ Status(response.status)(response.body)
+ .withHeaders(cacheHeaders: _*)
+ })
+ }
+
+ def getRecordSetCount(zoneId : String): Action[AnyContent] = userAction.async { implicit request =>
+ val vinyldnsRequest =
+ VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/$zoneId/recordsetcount")
+ executeRequest(vinyldnsRequest, request.user).map(response => {
+ Status(response.status)(response.body)
+ .withHeaders(cacheHeaders: _*)
+ })
+ }
+
def getAuthenticatedUserData(): Action[AnyContent] = userAction.async { implicit request =>
Future {
Ok(Json.toJson(VinylDNS.UserInfo.fromUser(request.user)))
@@ -308,7 +327,7 @@ class VinylDNS @Inject() (
.format(
user.userName,
user.accessKey,
- crypto.decrypt(user.secretKey),
+ crypto.decrypt(user.secretKey.value),
vinyldnsServiceBackend
)
).as("text/csv")
@@ -350,7 +369,7 @@ class VinylDNS @Inject() (
User(
details.username,
User.generateKey,
- User.generateKey,
+ Encrypted(User.generateKey),
details.firstName,
details.lastName,
details.email
@@ -448,6 +467,16 @@ class VinylDNS @Inject() (
// $COVERAGE-ON$
}
+ def getCommonZoneDetails(id: String): Action[AnyContent] = userAction.async { implicit request =>
+ // $COVERAGE-OFF$
+ val vinyldnsRequest = new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/$id/details")
+ executeRequest(vinyldnsRequest, request.user).map(response => {
+ Status(response.status)(response.body)
+ .withHeaders(cacheHeaders: _*)
+ })
+ // $COVERAGE-ON$
+ }
+
def getZoneByName(name: String): Action[AnyContent] = userAction.async { implicit request =>
val vinyldnsRequest =
new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"zones/name/$name")
@@ -457,6 +486,20 @@ class VinylDNS @Inject() (
})
}
+ def getDeletedZones: Action[AnyContent] = userAction.async { implicit request =>
+ val queryParameters = new HashMap[String, java.util.List[String]]()
+ for {
+ (name, values) <- request.queryString
+ } queryParameters.put(name, values.asJava)
+ val vinyldnsRequest =
+ new VinylDNSRequest("GET", s"$vinyldnsServiceBackend", "zones/deleted/changes", parameters = queryParameters)
+ executeRequest(vinyldnsRequest, request.user).map(response => {
+ Status(response.status)(response.body)
+ .withHeaders(cacheHeaders: _*)
+ })
+ // $COVERAGE-ON$
+ }
+
def getZoneChange(id: String): Action[AnyContent] = userAction.async { implicit request =>
val queryParameters = new HashMap[String, java.util.List[String]]()
for {
@@ -580,6 +623,25 @@ class VinylDNS @Inject() (
// $COVERAGE-ON$
}
+ def listRecordSetChangeHistory: Action[AnyContent] = userAction.async { implicit request =>
+ // $COVERAGE-OFF$
+ val queryParameters = new HashMap[String, java.util.List[String]]()
+ for {
+ (name, values) <- request.queryString
+ } queryParameters.put(name, values.asJava)
+ val vinyldnsRequest = new VinylDNSRequest(
+ "GET",
+ s"$vinyldnsServiceBackend",
+ s"recordsetchange/history",
+ parameters = queryParameters
+ )
+ executeRequest(vinyldnsRequest, request.user).map(response => {
+ Status(response.status)(response.body)
+ .withHeaders(cacheHeaders: _*)
+ })
+ // $COVERAGE-ON$
+ }
+
def addZone(): Action[AnyContent] = userAction.async { implicit request =>
// $COVERAGE-OFF$
val json = request.body.asJson
@@ -672,7 +734,7 @@ class VinylDNS @Inject() (
implicit userRequest: UserRequest[_]
) = {
val signableRequest = new SignableVinylDNSRequest(request)
- val credentials = new BasicAWSCredentials(user.accessKey, crypto.decrypt(user.secretKey))
+ val credentials = new BasicAWSCredentials(user.accessKey, crypto.decrypt(user.secretKey.value))
signer.sign(signableRequest, credentials)
logger.info(s"Request to send: [${signableRequest.getResourcePath}]")
diff --git a/modules/portal/app/models/Meta.scala b/modules/portal/app/models/Meta.scala
index f1e4cbf9a..b04e18cf0 100644
--- a/modules/portal/app/models/Meta.scala
+++ b/modules/portal/app/models/Meta.scala
@@ -23,7 +23,9 @@ case class Meta(
batchChangeLimit: Int,
defaultTtl: Long,
manualBatchChangeReviewEnabled: Boolean,
- scheduledBatchChangesEnabled: Boolean
+ scheduledBatchChangesEnabled: Boolean,
+ portalUrl: String,
+ maxGroupItemsDisplay: Int
)
object Meta {
def apply(config: Configuration): Meta =
@@ -33,6 +35,8 @@ object Meta {
config.getOptional[Int]("batch-change-limit").getOrElse(1000),
config.getOptional[Long]("default-ttl").getOrElse(7200L),
config.getOptional[Boolean]("manual-batch-review-enabled").getOrElse(false),
- config.getOptional[Boolean]("scheduled-changes-enabled").getOrElse(false)
+ config.getOptional[Boolean]("scheduled-changes-enabled").getOrElse(false),
+ config.getOptional[String]("portal.vinyldns.url").getOrElse("http://localhost:9001"),
+ config.getOptional[Int]("api.limits.membership-routing-max-groups-list-limit").getOrElse(3000)
)
}
diff --git a/modules/portal/app/views/dnsChanges/dnsChangeDetail.scala.html b/modules/portal/app/views/dnsChanges/dnsChangeDetail.scala.html
index 1d05c1613..7056936c5 100644
--- a/modules/portal/app/views/dnsChanges/dnsChangeDetail.scala.html
+++ b/modules/portal/app/views/dnsChanges/dnsChangeDetail.scala.html
@@ -41,7 +41,13 @@