2
0
mirror of https://github.com/VinylDNS/vinyldns synced 2025-08-22 10:10:12 +00:00

Merge branch 'master' into JoshSEdwards/documentation-for-apple-m1-support

This commit is contained in:
Nicholas Spadaccino 2022-10-11 16:13:36 -04:00 committed by GitHub
commit 41d9c91a3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2224 additions and 356 deletions

View File

@ -110,8 +110,8 @@ vinyldns {
limits { limits {
batchchange-routing-max-items-limit = 100 batchchange-routing-max-items-limit = 100
membership-routing-default-max-items = 100 membership-routing-default-max-items = 100
membership-routing-max-items-limit = 1000 membership-routing-max-items-limit = 100
membership-routing-max-groups-list-limit = 1500 membership-routing-max-groups-list-limit = 100
recordset-routing-default-max-items= 100 recordset-routing-default-max-items= 100
zone-routing-default-max-items = 100 zone-routing-default-max-items = 100
zone-routing-max-items-limit = 100 zone-routing-max-items-limit = 100

View File

@ -27,12 +27,15 @@ import vinyldns.api.domain.record.RecordSetChangeGenerator
import vinyldns.core.domain.record._ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.Zone import vinyldns.core.domain.zone.Zone
import vinyldns.core.domain.batch._ import vinyldns.core.domain.batch._
import vinyldns.core.domain.record.RecordType.RecordType import vinyldns.core.domain.record.RecordType.{RecordType, UNKNOWN}
import vinyldns.core.queue.MessageQueue import vinyldns.core.queue.MessageQueue
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue) class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
extends BatchChangeConverterAlgebra { 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]) private val logger = LoggerFactory.getLogger(classOf[BatchChangeConverter])
def sendBatchForProcessing( def sendBatchForProcessing(
@ -68,6 +71,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
): BatchResult[Unit] = { ): BatchResult[Unit] = {
val convertedIds = recordSetChanges.flatMap(_.singleBatchChangeIds).toSet val convertedIds = recordSetChanges.flatMap(_.singleBatchChangeIds).toSet
singleChanges.find(ch => !convertedIds.contains(ch.id)) match { singleChanges.find(ch => !convertedIds.contains(ch.id)) match {
// Each single change has a corresponding recordset id
// If they're not equal, then there's a delete request for a record that doesn't exist. So we allow this to process
case Some(_) if singleChanges.map(_.id).length != recordSetChanges.map(_.id).length && !singleChanges.map(_.typ).contains(UNKNOWN) =>
logger.info(s"Successfully converted SingleChanges [${singleChanges
.map(_.id)}] to RecordSetChanges [${recordSetChanges.map(_.id)}]")
().toRightBatchResult
case Some(change) => BatchConversionError(change).toLeftBatchResult case Some(change) => BatchConversionError(change).toLeftBatchResult
case None => case None =>
logger.info(s"Successfully converted SingleChanges [${singleChanges logger.info(s"Successfully converted SingleChanges [${singleChanges
@ -104,21 +113,33 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
rsChange.singleBatchChangeIds.map(batchId => (batchId, rsChange.id)) rsChange.singleBatchChangeIds.map(batchId => (batchId, rsChange.id))
}.toMap }.toMap
val withStatus = batchChange.changes.map { change => val withStatus = batchChange.changes.map { change =>
idsMap idsMap
.get(change.id) .get(change.id)
.map { _ => change } // a recordsetchange was successfully queued for this change .map { _ =>
.getOrElse { // a recordsetchange was successfully queued for this change
// failure here means there was a message queue issue for this change change
change.withFailureMessage("Error queueing RecordSetChange for processing") }
.getOrElse {
// Match and check if it's a delete change for a record that doesn't exists.
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)
case _ =>
// Failure here means there was a message queue issue for this change
change.withFailureMessage(failedMessage)
} }
} }
}
batchChange.copy(changes = withStatus) batchChange.copy(changes = withStatus)
} }
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = { def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
val failedChanges = batchChange.changes.collect { // Update if Single change is Failed or if a record that does not exist is deleted
case change if change.status == SingleChangeStatus.Failed => change } val failedAndNotExistsChanges = batchChange.changes.collect {
batchChangeRepo.updateSingleChanges(failedChanges).as(()) case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(notExistCompletedMessage) => change
}
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
}.toBatchResult }.toBatchResult
def createRecordSetChangesForBatch( def createRecordSetChangesForBatch(

View File

@ -348,7 +348,7 @@ class BatchChangeValidations(
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+| userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
zoneDoesNotRequireManualReview(change, isApproved) |+| zoneDoesNotRequireManualReview(change, isApproved) |+|
ensureRecordExists(change, groupedChanges) ensureRecordExists(change, groupedChanges)
case None => RecordDoesNotExist(change.inputChange.inputName).invalidNel case None => RecordDoesNotExist(change.inputChange.inputName).validNel
} }
validations.map(_ => change) validations.map(_ => change)
} }
@ -402,7 +402,7 @@ class BatchChangeValidations(
zoneDoesNotRequireManualReview(change, isApproved) |+| zoneDoesNotRequireManualReview(change, isApproved) |+|
ensureRecordExists(change, groupedChanges) ensureRecordExists(change, groupedChanges)
case None => case None =>
RecordDoesNotExist(change.inputChange.inputName).invalidNel RecordDoesNotExist(change.inputChange.inputName).validNel
} }
validations.map(_ => change) validations.map(_ => change)

View File

@ -59,7 +59,9 @@ final case class GroupChangeInfo(
userId: String, userId: String,
oldGroup: Option[GroupInfo] = None, oldGroup: Option[GroupInfo] = None,
id: String = UUID.randomUUID().toString, id: String = UUID.randomUUID().toString,
created: String = DateTime.now.getMillis.toString created: DateTime = DateTime.now,
userName: String,
groupChangeMessage: String
) )
object GroupChangeInfo { object GroupChangeInfo {
@ -69,7 +71,9 @@ object GroupChangeInfo {
userId = groupChange.userId, userId = groupChange.userId,
oldGroup = groupChange.oldGroup.map(GroupInfo.apply), oldGroup = groupChange.oldGroup.map(GroupInfo.apply),
id = groupChange.id, id = groupChange.id,
created = groupChange.created.getMillis.toString created = groupChange.created,
userName = groupChange.userName.getOrElse("unknown user"),
groupChangeMessage = groupChange.groupChangeMessage.getOrElse("")
) )
} }

View File

@ -216,12 +216,18 @@ class MembershipService(
): ListMyGroupsResponse = { ): ListMyGroupsResponse = {
val allMyGroups = allGroups val allMyGroups = allGroups
.filter(_.status == GroupStatus.Active) .filter(_.status == GroupStatus.Active)
.sortBy(_.id) .sortBy(_.name.toLowerCase)
.map(x => GroupInfo.fromGroup(x, abridged, Some(authPrincipal))) .map(x => GroupInfo.fromGroup(x, abridged, Some(authPrincipal)))
val filtered = allMyGroups val filtered = if(startFrom.isDefined){
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_))) val prevPageGroup = allMyGroups.filter(_.id == startFrom.get).head.name
.filter(grp => startFrom.forall(grp.id > _)) allMyGroups
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_)))
.filter(grp => grp.name.toLowerCase > prevPageGroup.toLowerCase)
} else {
allMyGroups
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_)))
}
val nextId = if (filtered.length > maxItems) Some(filtered(maxItems - 1).id) else None val nextId = if (filtered.length > maxItems) Some(filtered(maxItems - 1).id) else None
val groups = filtered.take(maxItems) val groups = filtered.take(maxItems)
@ -229,6 +235,23 @@ class MembershipService(
ListMyGroupsResponse(groups, groupNameFilter, startFrom, nextId, maxItems, ignoreAccess) ListMyGroupsResponse(groups, groupNameFilter, startFrom, nextId, maxItems, ignoreAccess)
} }
def getGroupChange(
groupChangeId: String,
authPrincipal: AuthPrincipal
): Result[GroupChangeInfo] =
for {
result <- groupChangeRepo
.getGroupChange(groupChangeId)
.toResult[Option[GroupChange]]
_ <- isGroupChangePresent(result).toResult
_ <- canSeeGroup(result.get.newGroup.id, authPrincipal).toResult
groupChangeMessage <- determineGroupDifference(Seq(result.get))
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)
userMap = users.map(u => (u.id, u.userName)).toMap
} yield groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).head
def getGroupActivity( def getGroupActivity(
groupId: String, groupId: String,
startFrom: Option[String], startFrom: Option[String],
@ -240,13 +263,65 @@ class MembershipService(
result <- groupChangeRepo result <- groupChangeRepo
.getGroupChanges(groupId, startFrom, maxItems) .getGroupChanges(groupId, startFrom, maxItems)
.toResult[ListGroupChangesResults] .toResult[ListGroupChangesResults]
groupChangeMessage <- determineGroupDifference(result.changes)
groupChanges = (groupChangeMessage, result.changes).zipped.map{ (a, b) => b.copy(groupChangeMessage = Some(a)) }
userIds = result.changes.map(_.userId).toSet
users <- getUsers(userIds).map(_.users)
userMap = users.map(u => (u.id, u.userName)).toMap
} yield ListGroupChangesResponse( } yield ListGroupChangesResponse(
result.changes.map(GroupChangeInfo.apply), groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
startFrom, startFrom,
result.lastEvaluatedTimeStamp, result.lastEvaluatedTimeStamp,
maxItems maxItems
) )
def determineGroupDifference(groupChange: Seq[GroupChange]): Result[Seq[String]] = {
var groupChangeMessage: Seq[String] = Seq.empty[String]
for (change <- groupChange) {
val sb = new StringBuilder
if (change.oldGroup.isDefined) {
if (change.oldGroup.get.name != change.newGroup.name) {
sb.append(s"Group name changed to '${change.newGroup.name}'. ")
}
if (change.oldGroup.get.email != change.newGroup.email) {
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 adminAddDifference = change.newGroup.adminUserIds.diff(change.oldGroup.get.adminUserIds)
if (adminAddDifference.nonEmpty) {
sb.append(s"Group admin/s with userId/s (${adminAddDifference.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. ")
}
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. ")
}
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. ")
}
groupChangeMessage = groupChangeMessage :+ sb.toString().trim
}
// It'll be in else statement if the group was created or deleted
else {
if (change.changeType == GroupChangeType.Create) {
sb.append("Group Created.")
}
else if (change.changeType == GroupChangeType.Delete){
sb.append("Group Deleted.")
}
groupChangeMessage = groupChangeMessage :+ sb.toString()
}
}
groupChangeMessage
}.toResult
/** /**
* Retrieves the requested User from the given userIdentifier, which can be a userId or username * Retrieves the requested User from the given userIdentifier, which can be a userId or username
* @param userIdentifier The userId or username * @param userIdentifier The userId or username

View File

@ -39,6 +39,8 @@ trait MembershipServiceAlgebra {
def getGroup(id: String, authPrincipal: AuthPrincipal): Result[Group] def getGroup(id: String, authPrincipal: AuthPrincipal): Result[Group]
def getGroupChange(id: String, authPrincipal: AuthPrincipal): Result[GroupChangeInfo]
def listMyGroups( def listMyGroups(
groupNameFilter: Option[String], groupNameFilter: Option[String],
startFrom: Option[String], startFrom: Option[String],

View File

@ -19,7 +19,7 @@ package vinyldns.api.domain.membership
import vinyldns.api.Interfaces.ensuring import vinyldns.api.Interfaces.ensuring
import vinyldns.core.domain.auth.AuthPrincipal import vinyldns.core.domain.auth.AuthPrincipal
import vinyldns.api.domain.zone.NotAuthorizedError import vinyldns.api.domain.zone.NotAuthorizedError
import vinyldns.core.domain.membership.Group import vinyldns.core.domain.membership.{Group, GroupChange}
object MembershipValidations { object MembershipValidations {
@ -44,4 +44,9 @@ object MembershipValidations {
ensuring(NotAuthorizedError("Not authorized")) { ensuring(NotAuthorizedError("Not authorized")) {
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails
} }
def isGroupChangePresent(groupChange: Option[GroupChange]): Either[Throwable, Unit] =
ensuring(InvalidGroupRequestError("Invalid Group Change ID")) {
groupChange.isDefined
}
} }

View File

@ -120,7 +120,9 @@ trait MembershipJsonProtocol extends JsonValidation {
(js \ "userId").required[String]("Missing userId"), (js \ "userId").required[String]("Missing userId"),
(js \ "oldGroup").optional[GroupInfo], (js \ "oldGroup").optional[GroupInfo],
(js \ "id").default[String](UUID.randomUUID().toString), (js \ "id").default[String](UUID.randomUUID().toString),
(js \ "created").default[String](DateTime.now.getMillis.toString) (js \ "created").default[DateTime](DateTime.now),
).mapN(GroupChangeInfo.apply) (js \ "userName").required[String]("Missing userName"),
(js \ "groupChangeMessage").required[String]("Missing groupChangeMessage"),
).mapN(GroupChangeInfo.apply)
} }
} }

View File

@ -80,7 +80,7 @@ class MembershipRoute(
} ~ } ~
(get & monitor("Endpoint.listMyGroups")) { (get & monitor("Endpoint.listMyGroups")) {
parameters( parameters(
"startFrom".?, "startFrom".as[String].?,
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS), "maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
"groupNameFilter".?, "groupNameFilter".?,
"ignoreAccess".as[Boolean].?(false), "ignoreAccess".as[Boolean].?(false),
@ -179,6 +179,13 @@ class MembershipRoute(
} }
} }
} ~ } ~
path("groups" / "change" / Segment) { groupChangeId =>
(get & monitor("Endpoint.groupSingleChange")) {
authenticateAndExecute(membershipService.getGroupChange(groupChangeId, _)) { groupChange =>
complete(StatusCodes.OK, groupChange)
}
}
} ~
path("users" / Segment / "lock") { id => path("users" / Segment / "lock") { id =>
(put & monitor("Endpoint.lockUser")) { (put & monitor("Endpoint.lockUser")) {
authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) { authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) {

View File

@ -1542,6 +1542,7 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json(rs_delete_fqdn, change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_fqdn, change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_fqdn, ttl=300), get_change_A_AAAA_json(rs_update_fqdn, ttl=300),
get_change_A_AAAA_json(f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures # input validations failures
get_change_A_AAAA_json("$invalid.host.name.", change_type="DeleteRecordSet"), get_change_A_AAAA_json("$invalid.host.name.", change_type="DeleteRecordSet"),
@ -1555,7 +1556,6 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json("zone.discovery.error.", change_type="DeleteRecordSet"), get_change_A_AAAA_json("zone.discovery.error.", change_type="DeleteRecordSet"),
# context validation failures: record does not exist, not authorized # context validation failures: record does not exist, not authorized
get_change_A_AAAA_json(f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_update_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, ttl=300), get_change_A_AAAA_json(rs_update_dummy_fqdn, ttl=300),
@ -1592,43 +1592,40 @@ def test_a_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, ttl=300) assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, ttl=300)
assert_successful_change_in_error_response(response[3], input_name=f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet")
# input validations failures # input validations failures
assert_failed_change_in_error_response(response[3], input_name="$invalid.host.name.", assert_failed_change_in_error_response(response[4], input_name="$invalid.host.name.",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, ' error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, '
'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) 'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name="reverse.zone.in-addr.arpa.", assert_failed_change_in_error_response(response[5], input_name="reverse.zone.in-addr.arpa.",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid Record Type In Reverse Zone: record with name "reverse.zone.in-addr.arpa." and type "A" ' error_messages=['Invalid Record Type In Reverse Zone: record with name "reverse.zone.in-addr.arpa." and type "A" '
'is not allowed in a reverse zone.']) 'is not allowed in a reverse zone.'])
assert_failed_change_in_error_response(response[5], input_name="$another.invalid.host.name.", ttl=300, assert_failed_change_in_error_response(response[6], input_name="$another.invalid.host.name.", ttl=300,
error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, ' error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, '
'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) 'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[6], input_name="$another.invalid.host.name.", assert_failed_change_in_error_response(response[7], input_name="$another.invalid.host.name.",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, ' error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, '
'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) 'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[7], input_name="another.reverse.zone.in-addr.arpa.", ttl=10, assert_failed_change_in_error_response(response[8], input_name="another.reverse.zone.in-addr.arpa.", ttl=10,
error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." ' error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." '
'and type "A" is not allowed in a reverse zone.', 'and type "A" is not allowed in a reverse zone.',
'Invalid TTL: "10", must be a number between 30 and 2147483647.']) 'Invalid TTL: "10", must be a number between 30 and 2147483647.'])
assert_failed_change_in_error_response(response[8], input_name="another.reverse.zone.in-addr.arpa.", assert_failed_change_in_error_response(response[9], input_name="another.reverse.zone.in-addr.arpa.",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." ' error_messages=['Invalid Record Type In Reverse Zone: record with name "another.reverse.zone.in-addr.arpa." '
'and type "A" is not allowed in a reverse zone.']) 'and type "A" is not allowed in a reverse zone.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[9], input_name="zone.discovery.error.", assert_failed_change_in_error_response(response[10], input_name="zone.discovery.error.",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Zone Discovery Failed: zone for "zone.discovery.error." does not exist in VinylDNS. ' error_messages=['Zone Discovery Failed: zone for "zone.discovery.error." does not exist in VinylDNS. '
'If zone exists, then it must be connected to in VinylDNS.']) 'If zone exists, then it must be connected to in VinylDNS.'])
# context validation failures: record does not exist, not authorized # context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[10], input_name=f"non-existent.{ok_zone_name}",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn, assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn,
change_type="DeleteRecordSet", 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.']) error_messages=[f'User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes.'])
@ -1779,6 +1776,8 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json(rs_delete_fqdn, record_type="AAAA", change_type="DeleteRecordSet", address="1:0::4:5:6:7:8"), get_change_A_AAAA_json(rs_delete_fqdn, record_type="AAAA", change_type="DeleteRecordSet", address="1:0::4:5:6:7:8"),
get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", ttl=300, address="1:2:3:4:5:6:7:8"), get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", ttl=300, address="1:2:3:4:5:6:7:8"),
get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_update_fqdn, record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"delete-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
# input validations failures # input validations failures
get_change_A_AAAA_json(f"invalid-name$.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"), get_change_A_AAAA_json(f"invalid-name$.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
@ -1790,8 +1789,6 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
get_change_A_AAAA_json("no.zone.at.all.", record_type="AAAA", change_type="DeleteRecordSet"), get_change_A_AAAA_json("no.zone.at.all.", record_type="AAAA", change_type="DeleteRecordSet"),
# context validation failures # context validation failures
get_change_A_AAAA_json(f"delete-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", address="1::1"), get_change_A_AAAA_json(f"update-nonexistent.{ok_zone_name}", record_type="AAAA", address="1::1"),
get_change_A_AAAA_json(rs_delete_dummy_fqdn, record_type="AAAA", change_type="DeleteRecordSet"), get_change_A_AAAA_json(rs_delete_dummy_fqdn, record_type="AAAA", change_type="DeleteRecordSet"),
get_change_A_AAAA_json(rs_update_dummy_fqdn, record_type="AAAA", address="1::1"), get_change_A_AAAA_json(rs_update_dummy_fqdn, record_type="AAAA", address="1::1"),
@ -1824,39 +1821,37 @@ def test_aaaa_recordtype_update_delete_checks(shared_zone_test_context):
record_data="1:2:3:4:5:6:7:8") record_data="1:2:3:4:5:6:7:8")
assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, record_type="AAAA", assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, record_type="AAAA",
record_data=None, change_type="DeleteRecordSet") record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl # input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="AAAA", assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", ' error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", '
f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) f'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[4], input_name="reverse.zone.in-addr.arpa.", record_type="AAAA", assert_failed_change_in_error_response(response[6], input_name="reverse.zone.in-addr.arpa.", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=["Invalid Record Type In Reverse Zone: record with name \"reverse.zone.in-addr.arpa.\" and " error_messages=["Invalid Record Type In Reverse Zone: record with name \"reverse.zone.in-addr.arpa.\" and "
"type \"AAAA\" is not allowed in a reverse zone."]) "type \"AAAA\" is not allowed in a reverse zone."])
assert_failed_change_in_error_response(response[5], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}", assert_failed_change_in_error_response(response[7], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}",
record_type="AAAA", record_data=None, change_type="DeleteRecordSet", record_type="AAAA", record_data=None, change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", ' error_messages=[f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", '
f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) f'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[6], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}", ttl=29, assert_failed_change_in_error_response(response[8], input_name=f"bad-ttl-and-invalid-name$-update.{ok_zone_name}", ttl=29,
record_type="AAAA", record_data="1:2:3:4:5:6:7:8", record_type="AAAA", record_data="1:2:3:4:5:6:7:8",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.', error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", ' f'Invalid domain name: "bad-ttl-and-invalid-name$-update.{ok_zone_name}", '
f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.']) f'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="AAAA", assert_failed_change_in_error_response(response[9], input_name="no.zone.at.all.", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in 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."]) "If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized # context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[8], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA", record_data="1::1") assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="AAAA", record_data="1::1")
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn, assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn,
record_type="AAAA", record_data=None, change_type="DeleteRecordSet", record_type="AAAA", record_data=None, change_type="DeleteRecordSet",
@ -2056,6 +2051,8 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
get_change_CNAME_json(f"delete3.{ok_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"delete3.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update3.{ok_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"update3.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update3.{ok_zone_name}", ttl=300), get_change_CNAME_json(f"update3.{ok_zone_name}", ttl=300),
get_change_CNAME_json(f"non-existent-delete.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}", change_type="DeleteRecordSet"),
# valid changes - reverse zone # valid changes - reverse zone
get_change_CNAME_json(f"200.{ip4_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"200.{ip4_zone_name}", change_type="DeleteRecordSet"),
@ -2071,8 +2068,6 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
get_change_CNAME_json("zone.discovery.error.", change_type="DeleteRecordSet"), get_change_CNAME_json("zone.discovery.error.", change_type="DeleteRecordSet"),
# context validation failures: record does not exist, not authorized, failure on update with multiple adds # context validation failures: record does not exist, not authorized, failure on update with multiple adds
get_change_CNAME_json(f"non-existent-delete.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"non-existent-update.{ok_zone_name}"), get_change_CNAME_json(f"non-existent-update.{ok_zone_name}"),
get_change_CNAME_json(f"delete-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"delete-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"),
get_change_CNAME_json(f"update-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"update-unauthorized3.{dummy_zone_name}", change_type="DeleteRecordSet"),
@ -2109,25 +2104,29 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
change_type="DeleteRecordSet") change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], input_name=f"update3.{ok_zone_name}", record_type="CNAME", ttl=300, assert_successful_change_in_error_response(response[2], input_name=f"update3.{ok_zone_name}", record_type="CNAME", ttl=300,
record_data="test.com.") record_data="test.com.")
assert_successful_change_in_error_response(response[3], input_name=f"non-existent-delete.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"non-existent-update.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet")
# valid changes - reverse zone # valid changes - reverse zone
assert_successful_change_in_error_response(response[3], input_name=f"200.{ip4_zone_name}", assert_successful_change_in_error_response(response[5], input_name=f"200.{ip4_zone_name}",
record_type="CNAME", change_type="DeleteRecordSet") record_type="CNAME", change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"201.{ip4_zone_name}", assert_successful_change_in_error_response(response[6], input_name=f"201.{ip4_zone_name}",
record_type="CNAME", change_type="DeleteRecordSet") record_type="CNAME", change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[5], input_name=f"201.{ip4_zone_name}", assert_successful_change_in_error_response(response[7], input_name=f"201.{ip4_zone_name}",
record_type="CNAME", ttl=300, record_data="test.com.") record_type="CNAME", ttl=300, record_data="test.com.")
# ttl, domain name, data # ttl, domain name, data
assert_failed_change_in_error_response(response[6], input_name="$invalid.host.name.", record_type="CNAME", assert_failed_change_in_error_response(response[8], input_name="$invalid.host.name.", record_type="CNAME",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, numbers, ' error_messages=['Invalid domain name: "$invalid.host.name.", valid domain names must be letters, numbers, '
'underscores, and hyphens, joined by dots, and terminated with a dot.']) 'underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[7], input_name="$another.invalid.host.name.", assert_failed_change_in_error_response(response[9], input_name="$another.invalid.host.name.",
record_type="CNAME", change_type="DeleteRecordSet", record_type="CNAME", change_type="DeleteRecordSet",
error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, numbers, ' error_messages=['Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, numbers, '
'underscores, and hyphens, joined by dots, and terminated with a dot.']) 'underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[8], input_name="$another.invalid.host.name.", ttl=20, assert_failed_change_in_error_response(response[10], input_name="$another.invalid.host.name.", ttl=20,
record_type="CNAME", record_data="$another.invalid.cname.", record_type="CNAME", record_data="$another.invalid.cname.",
error_messages=['Invalid TTL: "20", must be a number between 30 and 2147483647.', error_messages=['Invalid TTL: "20", must be a number between 30 and 2147483647.',
'Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, numbers, ' 'Invalid domain name: "$another.invalid.host.name.", valid domain names must be letters, numbers, '
@ -2136,20 +2135,12 @@ def test_cname_recordtype_update_delete_checks(shared_zone_test_context):
'underscores, and hyphens, joined by dots, and terminated with a dot.']) 'underscores, and hyphens, joined by dots, and terminated with a dot.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[9], input_name="zone.discovery.error.", record_type="CNAME", assert_failed_change_in_error_response(response[11], input_name="zone.discovery.error.", record_type="CNAME",
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=[ error_messages=[
'Zone Discovery Failed: zone for "zone.discovery.error." does not exist in VinylDNS. If zone exists, then it must be connected to in VinylDNS.']) 'Zone Discovery Failed: zone for "zone.discovery.error." 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 # context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[10], input_name=f"non-existent-delete.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent-delete.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(response[11], input_name=f"non-existent-update.{ok_zone_name}", record_type="CNAME",
change_type="DeleteRecordSet",
error_messages=[
f'Record "non-existent-update.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_successful_change_in_error_response(response[12], input_name=f"non-existent-update.{ok_zone_name}", assert_successful_change_in_error_response(response[12], input_name=f"non-existent-update.{ok_zone_name}",
record_type="CNAME", record_data="test.com.") record_type="CNAME", record_data="test.com.")
assert_failed_change_in_error_response(response[13], input_name=f"delete-unauthorized3.{dummy_zone_name}", assert_failed_change_in_error_response(response[13], input_name=f"delete-unauthorized3.{dummy_zone_name}",
@ -2383,6 +2374,8 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json(f"{ip4_prefix}.25", change_type="DeleteRecordSet"), get_change_PTR_json(f"{ip4_prefix}.25", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.193", ttl=300, ptrdname="has-updated.ptr."), get_change_PTR_json(f"{ip4_prefix}.193", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip4_prefix}.193", change_type="DeleteRecordSet"), get_change_PTR_json(f"{ip4_prefix}.193", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.199", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.200", change_type="DeleteRecordSet"),
# valid changes: delete and add of same record name but different type # valid changes: delete and add of same record name but different type
get_change_CNAME_json(f"21.{ip4_zone_name}", change_type="DeleteRecordSet"), get_change_CNAME_json(f"21.{ip4_zone_name}", change_type="DeleteRecordSet"),
@ -2399,9 +2392,7 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json("192.1.1.25", change_type="DeleteRecordSet"), get_change_PTR_json("192.1.1.25", change_type="DeleteRecordSet"),
# context validation failures # context validation failures
get_change_PTR_json(f"{ip4_prefix}.199", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip4_prefix}.200", ttl=300, ptrdname="has-updated.ptr."), get_change_PTR_json(f"{ip4_prefix}.200", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip4_prefix}.200", change_type="DeleteRecordSet"),
] ]
} }
@ -2422,25 +2413,29 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
record_data="has-updated.ptr.") record_data="has-updated.ptr.")
assert_successful_change_in_error_response(response[2], input_name=f"{ip4_prefix}.193", record_type="PTR", assert_successful_change_in_error_response(response[2], input_name=f"{ip4_prefix}.193", record_type="PTR",
record_data=None, change_type="DeleteRecordSet") record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"{ip4_prefix}.199", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.200", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
# successful changes: add and delete of same record name but different type # successful changes: add and delete of same record name but different type
assert_successful_change_in_error_response(response[3], input_name=f"21.{ip4_zone_name}", assert_successful_change_in_error_response(response[5], input_name=f"21.{ip4_zone_name}",
record_type="CNAME", record_data=None, change_type="DeleteRecordSet") record_type="CNAME", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip4_prefix}.21", record_type="PTR", assert_successful_change_in_error_response(response[6], input_name=f"{ip4_prefix}.21", record_type="PTR",
record_data="replace-cname.ptr.") record_data="replace-cname.ptr.")
assert_successful_change_in_error_response(response[5], input_name=f"17.{ip4_zone_name}", assert_successful_change_in_error_response(response[7], input_name=f"17.{ip4_zone_name}",
record_type="CNAME", record_data="replace-ptr.cname.") record_type="CNAME", record_data="replace-ptr.cname.")
assert_successful_change_in_error_response(response[6], input_name=f"{ip4_prefix}.17", record_type="PTR", assert_successful_change_in_error_response(response[8], input_name=f"{ip4_prefix}.17", record_type="PTR",
record_data=None, change_type="DeleteRecordSet") record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid IP, ttl, and record data # input validations failures: invalid IP, ttl, and record data
assert_failed_change_in_error_response(response[7], input_name="1.1.1", record_type="PTR", record_data=None, assert_failed_change_in_error_response(response[9], input_name="1.1.1", record_type="PTR", record_data=None,
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "1.1.1".']) error_messages=['Invalid IP address: "1.1.1".'])
assert_failed_change_in_error_response(response[8], input_name="192.0.2.", record_type="PTR", record_data=None, assert_failed_change_in_error_response(response[10], input_name="192.0.2.", record_type="PTR", record_data=None,
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "192.0.2.".']) error_messages=['Invalid IP address: "192.0.2.".'])
assert_failed_change_in_error_response(response[9], ttl=29, input_name="192.0.2.", record_type="PTR", assert_failed_change_in_error_response(response[11], ttl=29, input_name="192.0.2.", record_type="PTR",
record_data="failed-update$.ptr.", record_data="failed-update$.ptr.",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.', error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
'Invalid IP address: "192.0.2.".', 'Invalid IP address: "192.0.2.".',
@ -2448,19 +2443,13 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
'joined by dots, and terminated with a dot.']) 'joined by dots, and terminated with a dot.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[10], input_name="192.1.1.25", record_type="PTR", assert_failed_change_in_error_response(response[12], input_name="192.1.1.25", record_type="PTR",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"192.1.1.25\" does not exist in VinylDNS. If zone exists, " error_messages=["Zone Discovery Failed: zone for \"192.1.1.25\" does not exist in VinylDNS. If zone exists, "
"then it must be connected to in VinylDNS."]) "then it must be connected to in VinylDNS."])
# context validation failures: record does not exist # context validation failures: record does not exist
assert_failed_change_in_error_response(response[11], input_name=f"{ip4_prefix}.199", record_type="PTR", assert_successful_change_in_error_response(response[13], ttl=300, input_name=f"{ip4_prefix}.200", record_type="PTR", record_data="has-updated.ptr.")
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip4_prefix}.199\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[12], ttl=300, input_name=f"{ip4_prefix}.200", record_type="PTR", record_data="has-updated.ptr.")
assert_failed_change_in_error_response(response[13], input_name=f"{ip4_prefix}.200", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip4_prefix}.200\" Does Not Exist: cannot delete a record that does not exist."])
finally: finally:
clear_recordset_list(to_delete, ok_client) clear_recordset_list(to_delete, ok_client)
@ -2555,6 +2544,8 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json(f"{ip6_prefix}:1000::aaaa", change_type="DeleteRecordSet"), get_change_PTR_json(f"{ip6_prefix}:1000::aaaa", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::62", ttl=300, ptrdname="has-updated.ptr."), get_change_PTR_json(f"{ip6_prefix}:1000::62", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip6_prefix}:1000::62", change_type="DeleteRecordSet"), get_change_PTR_json(f"{ip6_prefix}:1000::62", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::60", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::65", change_type="DeleteRecordSet"),
# input validations failures # input validations failures
get_change_PTR_json("fd69:27cc:fe91de::ab", change_type="DeleteRecordSet"), get_change_PTR_json("fd69:27cc:fe91de::ab", change_type="DeleteRecordSet"),
@ -2565,9 +2556,7 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
get_change_PTR_json("fedc:ba98:7654::abc", change_type="DeleteRecordSet"), get_change_PTR_json("fedc:ba98:7654::abc", change_type="DeleteRecordSet"),
# context validation failures # context validation failures
get_change_PTR_json(f"{ip6_prefix}:1000::60", change_type="DeleteRecordSet"),
get_change_PTR_json(f"{ip6_prefix}:1000::65", ttl=300, ptrdname="has-updated.ptr."), get_change_PTR_json(f"{ip6_prefix}:1000::65", ttl=300, ptrdname="has-updated.ptr."),
get_change_PTR_json(f"{ip6_prefix}:1000::65", change_type="DeleteRecordSet")
] ]
} }
@ -2588,15 +2577,19 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
record_type="PTR", record_data="has-updated.ptr.") record_type="PTR", record_data="has-updated.ptr.")
assert_successful_change_in_error_response(response[2], input_name=f"{ip6_prefix}:1000::62", record_type="PTR", assert_successful_change_in_error_response(response[2], input_name=f"{ip6_prefix}:1000::62", record_type="PTR",
record_data=None, change_type="DeleteRecordSet") record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[3], input_name=f"{ip6_prefix}:1000::60", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"{ip6_prefix}:1000::65", record_type="PTR",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid IP, ttl, and record data # input validations failures: invalid IP, ttl, and record data
assert_failed_change_in_error_response(response[3], input_name="fd69:27cc:fe91de::ab", record_type="PTR", assert_failed_change_in_error_response(response[5], input_name="fd69:27cc:fe91de::ab", record_type="PTR",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "fd69:27cc:fe91de::ab".']) error_messages=['Invalid IP address: "fd69:27cc:fe91de::ab".'])
assert_failed_change_in_error_response(response[4], input_name="fd69:27cc:fe91de::ba", record_type="PTR", assert_failed_change_in_error_response(response[6], input_name="fd69:27cc:fe91de::ba", record_type="PTR",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=['Invalid IP address: "fd69:27cc:fe91de::ba".']) error_messages=['Invalid IP address: "fd69:27cc:fe91de::ba".'])
assert_failed_change_in_error_response(response[5], ttl=29, input_name="fd69:27cc:fe91de::ba", assert_failed_change_in_error_response(response[7], ttl=29, input_name="fd69:27cc:fe91de::ba",
record_type="PTR", record_data="failed-update$.ptr.", record_type="PTR", record_data="failed-update$.ptr.",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.', error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
'Invalid IP address: "fd69:27cc:fe91de::ba".', 'Invalid IP address: "fd69:27cc:fe91de::ba".',
@ -2604,20 +2597,14 @@ def test_ipv6_ptr_recordtype_update_delete_checks(shared_zone_test_context):
'and hyphens, joined by dots, and terminated with a dot.']) 'and hyphens, joined by dots, and terminated with a dot.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[6], input_name="fedc:ba98:7654::abc", record_type="PTR", assert_failed_change_in_error_response(response[8], input_name="fedc:ba98:7654::abc", record_type="PTR",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"fedc:ba98:7654::abc\" does not exist in VinylDNS. " error_messages=["Zone Discovery Failed: zone for \"fedc:ba98:7654::abc\" does not exist in VinylDNS. "
"If zone exists, then it must be connected to in VinylDNS."]) "If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, failure on update with double add # context validation failures: record does not exist, failure on update with double add
assert_failed_change_in_error_response(response[7], input_name=f"{ip6_prefix}:1000::60", record_type="PTR", assert_successful_change_in_error_response(response[9], ttl=300, input_name=f"{ip6_prefix}:1000::65",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip6_prefix}:1000::60\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[8], ttl=300, input_name=f"{ip6_prefix}:1000::65",
record_type="PTR", record_data="has-updated.ptr.") record_type="PTR", record_data="has-updated.ptr.")
assert_failed_change_in_error_response(response[9], input_name=f"{ip6_prefix}:1000::65", record_type="PTR",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"{ip6_prefix}:1000::65\" Does Not Exist: cannot delete a record that does not exist."])
finally: finally:
clear_recordset_list(to_delete, ok_client) clear_recordset_list(to_delete, ok_client)
@ -2744,6 +2731,8 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
get_change_TXT_json(rs_delete_fqdn, change_type="DeleteRecordSet"), get_change_TXT_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_fqdn, change_type="DeleteRecordSet"), get_change_TXT_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_fqdn, ttl=300), get_change_TXT_json(rs_update_fqdn, ttl=300),
get_change_TXT_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures # input validations failures
get_change_TXT_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"), get_change_TXT_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"),
@ -2753,8 +2742,6 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
get_change_TXT_json("no.zone.at.all.", change_type="DeleteRecordSet"), get_change_TXT_json("no.zone.at.all.", change_type="DeleteRecordSet"),
# context validation failures # context validation failures
get_change_TXT_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", text="test"), get_change_TXT_json(f"update-nonexistent.{ok_zone_name}", text="test"),
get_change_TXT_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"), get_change_TXT_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_TXT_json(rs_update_dummy_fqdn, text="test"), get_change_TXT_json(rs_update_dummy_fqdn, text="test"),
@ -2784,25 +2771,23 @@ def test_txt_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="TXT", record_data="test") assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="TXT", record_data="test")
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl # input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="TXT", record_data="test", change_type="DeleteRecordSet", assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="TXT", record_data="test", change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be ' 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.']) f'letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="TXT", record_data="bad-ttl", assert_failed_change_in_error_response(response[6], input_name=f"invalid-ttl.{ok_zone_name}", ttl=29, record_type="TXT", record_data="bad-ttl",
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.']) error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[5], input_name="no.zone.at.all.", record_type="TXT", record_data=None, change_type="DeleteRecordSet", assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[ error_messages=[
"Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. " "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."]) "If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized # context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[6], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[7], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[8], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data="test") assert_successful_change_in_error_response(response[8], input_name=f"update-nonexistent.{ok_zone_name}", record_type="TXT", record_data="test")
assert_failed_change_in_error_response(response[9], input_name=rs_delete_dummy_fqdn, record_type="TXT", record_data=None, change_type="DeleteRecordSet", assert_failed_change_in_error_response(response[9], input_name=rs_delete_dummy_fqdn, record_type="TXT", 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."]) error_messages=[f"User \"ok\" is not authorized. Contact zone owner group: {dummy_group_name} at test@test.com to make DNS changes."])
@ -2953,6 +2938,8 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
get_change_MX_json(rs_delete_fqdn, change_type="DeleteRecordSet"), get_change_MX_json(rs_delete_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_fqdn, change_type="DeleteRecordSet"), get_change_MX_json(rs_update_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_fqdn, ttl=300), get_change_MX_json(rs_update_fqdn, ttl=300),
get_change_MX_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
# input validations failures # input validations failures
get_change_MX_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"), get_change_MX_json(f"invalid-name$.{ok_zone_name}", change_type="DeleteRecordSet"),
@ -2964,8 +2951,6 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
get_change_MX_json("no.zone.at.all.", change_type="DeleteRecordSet"), get_change_MX_json("no.zone.at.all.", change_type="DeleteRecordSet"),
# context validation failures # context validation failures
get_change_MX_json(f"delete-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", change_type="DeleteRecordSet"),
get_change_MX_json(f"update-nonexistent.{ok_zone_name}", preference=1000, exchange="foo.bar."), get_change_MX_json(f"update-nonexistent.{ok_zone_name}", preference=1000, exchange="foo.bar."),
get_change_MX_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"), get_change_MX_json(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
get_change_MX_json(rs_update_dummy_fqdn, preference=1000, exchange="foo.bar."), get_change_MX_json(rs_update_dummy_fqdn, preference=1000, exchange="foo.bar."),
@ -2995,37 +2980,35 @@ def test_mx_recordtype_update_delete_checks(shared_zone_test_context):
assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[0], input_name=rs_delete_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet") assert_successful_change_in_error_response(response[1], input_name=rs_update_fqdn, record_type="MX", record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."}) assert_successful_change_in_error_response(response[2], ttl=300, input_name=rs_update_fqdn, record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."})
assert_successful_change_in_error_response(response[3], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet")
assert_successful_change_in_error_response(response[4], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet")
# input validations failures: invalid input name, reverse zone error, invalid ttl # input validations failures: invalid input name, reverse zone error, invalid ttl
assert_failed_change_in_error_response(response[3], input_name=f"invalid-name$.{ok_zone_name}", record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."}, assert_failed_change_in_error_response(response[5], input_name=f"invalid-name$.{ok_zone_name}", record_type="MX", record_data={"preference": 1, "exchange": "foo.bar."},
change_type="DeleteRecordSet", change_type="DeleteRecordSet",
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be letters, ' 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.']) f'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[4], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="MX", assert_failed_change_in_error_response(response[6], input_name=f"delete.{ok_zone_name}", ttl=29, record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."}, record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.']) error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.'])
assert_failed_change_in_error_response(response[5], input_name=f"bad-exchange.{ok_zone_name}", record_type="MX", assert_failed_change_in_error_response(response[7], input_name=f"bad-exchange.{ok_zone_name}", record_type="MX",
record_data={"preference": 1, "exchange": "foo$.bar."}, record_data={"preference": 1, "exchange": "foo$.bar."},
error_messages=['Invalid domain name: "foo$.bar.", valid domain names must be letters, numbers, ' error_messages=['Invalid domain name: "foo$.bar.", valid domain names must be letters, numbers, '
'underscores, and hyphens, joined by dots, and terminated with a dot.']) 'underscores, and hyphens, joined by dots, and terminated with a dot.'])
assert_failed_change_in_error_response(response[6], input_name=f"mx.{ip4_zone_name}", record_type="MX", assert_failed_change_in_error_response(response[8], input_name=f"mx.{ip4_zone_name}", record_type="MX",
record_data={"preference": 1, "exchange": "foo.bar."}, record_data={"preference": 1, "exchange": "foo.bar."},
error_messages=[f'Invalid Record Type In Reverse Zone: record with name "mx.{ip4_zone_name}" ' error_messages=[f'Invalid Record Type In Reverse Zone: record with name "mx.{ip4_zone_name}" '
f'and type "MX" is not allowed in a reverse zone.']) f'and type "MX" is not allowed in a reverse zone.'])
# zone discovery failure # zone discovery failure
assert_failed_change_in_error_response(response[7], input_name="no.zone.at.all.", record_type="MX", assert_failed_change_in_error_response(response[9], input_name="no.zone.at.all.", record_type="MX",
record_data=None, change_type="DeleteRecordSet", record_data=None, change_type="DeleteRecordSet",
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in 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."]) "If zone exists, then it must be connected to in VinylDNS."])
# context validation failures: record does not exist, not authorized # context validation failures: record does not exist, not authorized
assert_failed_change_in_error_response(response[8], input_name=f"delete-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"delete-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_failed_change_in_error_response(response[9], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data=None, change_type="DeleteRecordSet",
error_messages=[f"Record \"update-nonexistent.{ok_zone_name}\" Does Not Exist: cannot delete a record that does not exist."])
assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX", assert_successful_change_in_error_response(response[10], input_name=f"update-nonexistent.{ok_zone_name}", record_type="MX",
record_data={"preference": 1000, "exchange": "foo.bar."}) record_data={"preference": 1000, "exchange": "foo.bar."})
assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn, record_type="MX", assert_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn, record_type="MX",
@ -3734,39 +3717,27 @@ def test_create_batch_with_zone_name_requiring_manual_review(shared_zone_test_co
rejecter.reject_batch_change(response["id"], status=200) rejecter.reject_batch_change(response["id"], status=200)
def test_create_batch_delete_record_for_invalid_record_data_fails(shared_zone_test_context): def test_create_batch_delete_record_that_does_not_exists_completes(shared_zone_test_context):
""" """
Test delete record set fails for non-existent record and non-existent record data Test delete record set completes for non-existent record
""" """
client = shared_zone_test_context.ok_vinyldns_client client = shared_zone_test_context.ok_vinyldns_client
ok_zone_name = shared_zone_test_context.ok_zone["name"] ok_zone_name = shared_zone_test_context.ok_zone["name"]
a_delete_name = generate_record_name()
a_delete_fqdn = a_delete_name + f".{ok_zone_name}"
a_delete = create_recordset(shared_zone_test_context.ok_zone, a_delete_fqdn, "A", [{"address": "1.1.1.1"}])
batch_change_input = { batch_change_input = {
"comments": "test delete record failures", "comments": "test delete record failures",
"changes": [ "changes": [
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet"), get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet")
get_change_A_AAAA_json(a_delete_fqdn, address="4.5.6.7", change_type="DeleteRecordSet")
] ]
} }
to_delete = [] response = client.create_batch_change(batch_change_input, status=202)
get_batch = client.get_batch_change(response["id"])
try: assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist." +
create_rs = client.create_recordset(a_delete, status=202) "No further action is required."))
to_delete.append(client.wait_until_recordset_change_status(create_rs, "Complete"))
errors = client.create_batch_change(batch_change_input, status=400) 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")
assert_failed_change_in_error_response(errors[0], input_name=f"delete-non-existent-record.{ok_zone_name}", record_data="1.1.1.1", change_type="DeleteRecordSet",
error_messages=[f'Record "delete-non-existent-record.{ok_zone_name}" Does Not Exist: cannot delete a record that does not exist.'])
assert_failed_change_in_error_response(errors[1], input_name=a_delete_fqdn, record_data="4.5.6.7", change_type="DeleteRecordSet",
error_messages=["Record data 4.5.6.7 does not exist for \"" + a_delete_fqdn + "\"."])
finally:
clear_recordset_list(to_delete, client)
@pytest.mark.serial @pytest.mark.serial

View File

@ -26,16 +26,12 @@ 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 # 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) 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
start_from = page_one["changes"][start_from_index]["created"] # start from a known good timestamp
# now, we say give me all changes since the start_from, which should yield 8-7-6-5-4 # 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=start_from, max_items=5, status=200) result = client.get_group_changes(created_group["id"], start_from=page_one["nextId"], max_items=5, status=200)
assert_that(result["changes"], has_length(5)) assert_that(result["changes"], has_length(5))
assert_that(result["maxItems"], is_(5)) assert_that(result["maxItems"], is_(5))
assert_that(result["startFrom"], is_(start_from)) assert_that(result["startFrom"], is_(page_one["nextId"]))
assert_that(result["nextId"], is_not(none())) assert_that(result["nextId"], is_not(none()))
# we should have, in order, changes 8 7 6 5 4 # we should have, in order, changes 8 7 6 5 4

View File

@ -13,20 +13,11 @@ def test_list_my_groups_no_parameters(list_my_groups_context):
Test that we can get all the groups where a user is a member Test that we can get all the groups where a user is a member
""" """
results = list_my_groups_context.client.list_my_groups(status=200) results = list_my_groups_context.client.list_my_groups(status=200)
assert_that(results, has_length(3)) # 3 fields assert_that(results, has_length(4)) # 4 fields
# Only count the groups with the group prefix
groups = [x for x in results["groups"] if x["name"].startswith(list_my_groups_context.group_prefix)]
assert_that(groups, has_length(50))
assert_that(results, is_not(has_key("groupNameFilter"))) assert_that(results, is_not(has_key("groupNameFilter")))
assert_that(results, is_not(has_key("startFrom"))) assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId"))) assert_that(results, is_(has_key("nextId")))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
results["groups"] = sorted(groups, key=lambda x: x["name"])
for i in range(0, 50):
assert_that(results["groups"][i]["name"], is_("{0}-{1:0>3}".format(list_my_groups_context.group_prefix, i)))
def test_get_my_groups_using_old_account_auth(list_my_groups_context): def test_get_my_groups_using_old_account_auth(list_my_groups_context):
@ -34,11 +25,11 @@ def test_get_my_groups_using_old_account_auth(list_my_groups_context):
Test passing in an account will return an empty set Test passing in an account will return an empty set
""" """
results = list_my_groups_context.client.list_my_groups(status=200) results = list_my_groups_context.client.list_my_groups(status=200)
assert_that(results, has_length(3)) assert_that(results, has_length(4))
assert_that(results, is_not(has_key("groupNameFilter"))) assert_that(results, is_not(has_key("groupNameFilter")))
assert_that(results, is_not(has_key("startFrom"))) assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId"))) assert_that(results, is_(has_key("nextId")))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
def test_list_my_groups_max_items(list_my_groups_context): def test_list_my_groups_max_items(list_my_groups_context):
@ -102,7 +93,7 @@ def test_list_my_groups_filter_matches(list_my_groups_context):
assert_that(results["groupNameFilter"], is_(f"{list_my_groups_context.group_prefix}-01")) assert_that(results["groupNameFilter"], is_(f"{list_my_groups_context.group_prefix}-01"))
assert_that(results, is_not(has_key("startFrom"))) assert_that(results, is_not(has_key("startFrom")))
assert_that(results, is_not(has_key("nextId"))) assert_that(results, is_not(has_key("nextId")))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
results["groups"] = sorted(results["groups"], key=lambda x: x["name"]) results["groups"] = sorted(results["groups"], key=lambda x: x["name"])
@ -133,28 +124,20 @@ def test_list_my_groups_with_ignore_access_true(list_my_groups_context):
Test that we can get all the groups whether a user is a member or not Test that we can get all the groups whether a user is a member or not
""" """
results = list_my_groups_context.client.list_my_groups(ignore_access=True, status=200) results = list_my_groups_context.client.list_my_groups(ignore_access=True, status=200)
assert_that(results, has_length(4)) # 4 fields
# Only count the groups with the group prefix
assert_that(len(results["groups"]), greater_than(50)) assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(True)) assert_that(results["ignoreAccess"], is_(True))
my_results = list_my_groups_context.client.list_my_groups(status=200)
my_groups = [x for x in my_results["groups"] if x["name"].startswith(list_my_groups_context.group_prefix)]
sorted_groups = sorted(my_groups, key=lambda x: x["name"])
for i in range(0, 50):
assert_that(sorted_groups[i]["name"], is_("{0}-{1:0>3}".format(list_my_groups_context.group_prefix, i)))
def test_list_my_groups_as_support_user(list_my_groups_context): def test_list_my_groups_as_support_user(list_my_groups_context):
""" """
Test that we can get all the groups as a support user, even without ignore_access Test that we can get all the groups as a support user, even without ignore_access
""" """
results = list_my_groups_context.support_user_client.list_my_groups(status=200) results = list_my_groups_context.support_user_client.list_my_groups(status=200)
assert_that(results, has_length(4)) # 4 fields
assert_that(len(results["groups"]), greater_than(50)) assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(False)) assert_that(results["ignoreAccess"], is_(False))
@ -163,7 +146,7 @@ def test_list_my_groups_as_support_user_with_ignore_access_true(list_my_groups_c
Test that we can get all the groups as a support user Test that we can get all the groups as a support user
""" """
results = list_my_groups_context.support_user_client.list_my_groups(ignore_access=True, status=200) results = list_my_groups_context.support_user_client.list_my_groups(ignore_access=True, status=200)
assert_that(results, has_length(4)) # 4 fields
assert_that(len(results["groups"]), greater_than(50)) assert_that(len(results["groups"]), greater_than(50))
assert_that(results["maxItems"], is_(200)) assert_that(results["maxItems"], is_(100))
assert_that(results["ignoreAccess"], is_(True)) assert_that(results["ignoreAccess"], is_(True))

View File

@ -220,7 +220,7 @@ class VinylDNSClient(object):
return data return data
def list_my_groups(self, group_name_filter=None, start_from=None, max_items=200, ignore_access=False, **kwargs): def list_my_groups(self, group_name_filter=None, start_from=None, max_items=100, ignore_access=False, **kwargs):
""" """
Retrieves my groups Retrieves my groups
:param start_from: the start key of the page :param start_from: the start key of the page

View File

@ -37,6 +37,8 @@ import vinyldns.core.domain.record._
import vinyldns.core.domain.zone.Zone import vinyldns.core.domain.zone.Zone
class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelpers { class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelpers {
private val notExistCompletedMessage: String = "This record does not exist." +
"No further action is required."
private def makeSingleAddChange( private def makeSingleAddChange(
name: String, name: String,
@ -160,6 +162,14 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
makeAddChangeForValidation("mxToUpdate", MXData(1, Fqdn("update.com.")), MX) makeAddChangeForValidation("mxToUpdate", MXData(1, Fqdn("update.com.")), MX)
) )
private val singleChangesOneDelete = List(
makeSingleDeleteRRSetChange("DoesNotExistToDelete", A)
)
private val changeForValidationOneDelete = List(
makeDeleteRRSetChangeForValidation("DoesNotExistToDelete", A)
)
private val singleChangesOneBad = List( private val singleChangesOneBad = List(
makeSingleAddChange("one", AData("1.1.1.1")), makeSingleAddChange("one", AData("1.1.1.1")),
makeSingleAddChange("two", AData("1.1.1.2")), makeSingleAddChange("two", AData("1.1.1.2")),
@ -535,6 +545,42 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
savedBatch shouldBe Some(returnedBatch) savedBatch shouldBe Some(returnedBatch)
} }
"set status to complete when deleting a record that does not exist" in {
val batchWithBadChange =
BatchChange(
okUser.id,
okUser.userName,
None,
DateTime.now,
singleChangesOneDelete,
approvalStatus = BatchChangeApprovalStatus.AutoApproved
)
val result = rightResultOf(
underTest
.sendBatchForProcessing(
batchWithBadChange,
existingZones,
ChangeForValidationMap(changeForValidationOneDelete.map(_.validNel), existingRecordSets),
None
)
.value
)
val returnedBatch = result.batchChange
// validate completed status returned
val receivedChange = returnedBatch.changes(0)
receivedChange.status shouldBe SingleChangeStatus.Complete
receivedChange.recordChangeId shouldBe None
receivedChange.systemMessage shouldBe Some(notExistCompletedMessage)
returnedBatch.changes(0) shouldBe singleChangesOneDelete(0).copy(systemMessage = Some(notExistCompletedMessage), status = SingleChangeStatus.Complete)
// check the update has been made in the DB
val savedBatch: Option[BatchChange] =
await(batchChangeRepo.getBatchChange(batchWithBadChange.id))
savedBatch shouldBe Some(returnedBatch)
}
"return error if an unsupported record is received" in { "return error if an unsupported record is received" in {
val batchChangeUnsupported = val batchChangeUnsupported =
BatchChange( BatchChange(

View File

@ -1004,7 +1004,7 @@ class BatchChangeValidationsSpec
) )
} }
property("validateChangesWithContext: should fail for update if record does not exist") { property("validateChangesWithContext: should complete for update if record does not exist") {
val deleteRRSet = makeDeleteUpdateDeleteRRSet("deleteRRSet") val deleteRRSet = makeDeleteUpdateDeleteRRSet("deleteRRSet")
val deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", Some(AData("1.1.1.1"))) val deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", Some(AData("1.1.1.1")))
val deleteNonExistentEntry = makeDeleteUpdateDeleteRRSet("ok", Some(AData("1.1.1.1"))) val deleteNonExistentEntry = makeDeleteUpdateDeleteRRSet("ok", Some(AData("1.1.1.1")))
@ -1026,15 +1026,8 @@ class BatchChangeValidationsSpec
) )
result(0) shouldBe valid result(0) shouldBe valid
result(1) should haveInvalid[DomainValidationError]( result(1) shouldBe valid
RecordDoesNotExist(deleteRRSet.inputChange.inputName) result(3) shouldBe valid
)
result(3) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
result(3) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
result(4) shouldBe valid result(4) shouldBe valid
deleteNonExistentEntry.inputChange.record.foreach { record => deleteNonExistentEntry.inputChange.record.foreach { record =>
result(5) should haveInvalid[DomainValidationError]( result(5) should haveInvalid[DomainValidationError](
@ -1589,7 +1582,7 @@ class BatchChangeValidationsSpec
} }
property( property(
"""validateChangesWithContext: should fail DeleteChangeForValidation with RecordDoesNotExist """validateChangesWithContext: should complete DeleteChangeForValidation
|if record does not exist""".stripMargin |if record does not exist""".stripMargin
) { ) {
val deleteRRSet = makeDeleteUpdateDeleteRRSet("record-does-not-exist") val deleteRRSet = makeDeleteUpdateDeleteRRSet("record-does-not-exist")
@ -1606,12 +1599,8 @@ class BatchChangeValidationsSpec
None None
) )
result(0) should haveInvalid[DomainValidationError]( result(0) shouldBe valid
RecordDoesNotExist(deleteRRSet.inputChange.inputName) result(1) shouldBe valid
)
result(1) should haveInvalid[DomainValidationError](
RecordDoesNotExist(deleteRecord.inputChange.inputName)
)
} }
property("""validateChangesWithContext: should succeed for DeleteChangeForValidation property("""validateChangesWithContext: should succeed for DeleteChangeForValidation

View File

@ -783,6 +783,59 @@ class MembershipServiceSpec
} }
} }
"getGroupChange" should {
"return the single group change" in {
val groupChangeRepoResponse = listOfDummyGroupChanges.take(1).head
doReturn(IO.pure(Option(groupChangeRepoResponse)))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
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: GroupChangeInfo =
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)
result shouldBe expected
}
"return the single group change even if the user is not authorized" in {
val groupChangeRepoResponse = listOfDummyGroupChanges.take(1).head
doReturn(IO.pure(Some(groupChangeRepoResponse)))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
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: 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
}
"return a InvalidGroupRequestError if the group change id is not valid" in {
doReturn(IO.pure(None))
.when(mockGroupChangeRepo)
.getGroupChange(anyString)
doReturn(IO.pure(ListUsersResults(Seq(dummyUser), Some("1"))))
.when(mockUserRepo)
.getUsers(any[Set[String]], any[Option[String]], any[Option[Int]])
val result = leftResultOf(underTest.getGroupChange(dummyGroup.id, okAuth).value)
result shouldBe a[InvalidGroupRequestError]
}
}
"getGroupActivity" should { "getGroupActivity" should {
"return the group activity" in { "return the group activity" in {
val groupChangeRepoResponse = ListGroupChangesResults( val groupChangeRepoResponse = ListGroupChangesResults(
@ -793,8 +846,13 @@ class MembershipServiceSpec
.when(mockGroupChangeRepo) .when(mockGroupChangeRepo)
.getGroupChanges(anyString, any[Option[String]], anyInt) .getGroupChanges(anyString, any[Option[String]], 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] = val expected: List[GroupChangeInfo] =
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100) listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
val result: ListGroupChangesResponse = val result: ListGroupChangesResponse =
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value) rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value)
@ -813,8 +871,13 @@ class MembershipServiceSpec
.when(mockGroupChangeRepo) .when(mockGroupChangeRepo)
.getGroupChanges(anyString, any[Option[String]], anyInt) .getGroupChanges(anyString, any[Option[String]], 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] = val expected: List[GroupChangeInfo] =
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100) listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
val result: ListGroupChangesResponse = val result: ListGroupChangesResponse =
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value) rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value)
@ -825,6 +888,19 @@ class MembershipServiceSpec
} }
} }
"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)
// 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."
// Deleted group's change message
result(2) shouldBe "Group Deleted."
}
}
"listAdmins" should { "listAdmins" should {
"return a list of admins" in { "return a list of admins" in {
val testGroup = val testGroup =

View File

@ -96,5 +96,16 @@ class MembershipValidationsSpec
} }
} }
"isGroupChangePresent" should {
"return true when there is a group change present for the requested group change id" in {
isGroupChangePresent(Some(okGroupChange)) should be(right)
}
"return an error when there is a group change present for the requested group change id" in {
val error = leftValue(isGroupChangePresent(None))
error shouldBe an[InvalidGroupRequestError]
}
}
} }
} }

View File

@ -730,6 +730,37 @@ class MembershipRoutingSpec
} }
} }
"GET group change" should {
"return a 200 response with the group change when found" in {
val grpChange = GroupChangeInfo(okGroupChange)
doReturn(result(grpChange)).when(membershipService).getGroupChange("ok", okAuth)
Get("/groups/change/ok") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.OK
val result = responseAs[GroupChangeInfo]
result shouldBe grpChange
}
}
"return a 400 Bad Request when the group change id is not valid" in {
doReturn(result(InvalidGroupRequestError("Invalid Group Change ID")))
.when(membershipService)
.getGroupChange("notValid", okAuth)
Get("/groups/change/notValid") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.BadRequest
}
}
"return a 500 response on failure" in {
doReturn(result(new RuntimeException("fail")))
.when(membershipService)
.getGroupChange("bad", okAuth)
Get(s"/groups/change/bad") ~> Route.seal(membershipRoute) ~> check {
status shouldBe StatusCodes.InternalServerError
}
}
}
"PUT update user lock status" should { "PUT update user lock status" should {
"return a 200 response with the user locked" in { "return a 200 response with the user locked" in {
membershipRoute = superUserRoute membershipRoute = superUserRoute

View File

@ -46,6 +46,13 @@ sealed trait SingleChange {
delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error)) delete.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
} }
def withDoesNotExistMessage(error: String): SingleChange = this match {
case add: SingleAddChange =>
add.copy(status = SingleChangeStatus.Failed, systemMessage = Some(error))
case delete: SingleDeleteRRSetChange =>
delete.copy(status = SingleChangeStatus.Complete, systemMessage = Some(error))
}
def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange = def withProcessingError(message: Option[String], failedRecordChangeId: String): SingleChange =
this match { this match {
case add: SingleAddChange => case add: SingleAddChange =>

View File

@ -34,7 +34,9 @@ case class GroupChange(
userId: String, userId: String,
oldGroup: Option[Group] = None, oldGroup: Option[Group] = None,
id: String = UUID.randomUUID().toString, id: String = UUID.randomUUID().toString,
created: DateTime = DateTime.now created: DateTime = DateTime.now,
userName: Option[String] = None,
groupChangeMessage: Option[String] = None
) )
object GroupChange { object GroupChange {

View File

@ -24,7 +24,8 @@ import vinyldns.core.repository.Repository
trait GroupChangeRepository extends Repository { trait GroupChangeRepository extends Repository {
def save(db: DB, groupChange: GroupChange): IO[GroupChange] def save(db: DB, groupChange: GroupChange): IO[GroupChange]
def getGroupChange(groupChangeId: String): IO[Option[GroupChange]] // For testing def getGroupChange(groupChangeId: String): IO[Option[GroupChange]]
def getGroupChanges( def getGroupChanges(
groupId: String, groupId: String,
startFrom: Option[String], startFrom: Option[String],

View File

@ -157,4 +157,13 @@ object TestMembershipData {
id = s"$i" id = s"$i"
) )
} }
val dummyGroupChangeUpdate: GroupChange = GroupChange(
okGroup.copy(name = "dummy-group", email = "dummy@test.com", description = Some("dummy group"),
memberIds = Set(dummyUser.copy(id="12345-abcde-6789").id, superUser.copy(id="56789-edcba-1234").id),
adminUserIds = Set(dummyUser.copy(id="12345-abcde-6789").id, superUser.copy(id="56789-edcba-1234").id)),
GroupChangeType.Update,
okUser.id,
Some(okGroup),
created = DateTime.now.secondOfDay().roundFloorCopy()
)
} }

View File

@ -214,6 +214,32 @@ class VinylDNS @Inject() (
}) })
} }
def getGroupChange(gcid: String): Action[AnyContent] = userAction.async { implicit request =>
val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"groups/change/$gcid")
executeRequest(vinyldnsRequest, request.user).map(response => {
logger.info(s"group change [$gcid] retrieved with status [${response.status}]")
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def listGroupChanges(id: String): 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",
s"groups/$id/activity",
parameters = queryParameters
)
executeRequest(vinyldnsRequest, request.user).map(response => {
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def getUser(id: String): Action[AnyContent] = userAction.async { implicit request => def getUser(id: String): Action[AnyContent] = userAction.async { implicit request =>
val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"users/$id") val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"users/$id")
executeRequest(vinyldnsRequest, request.user).map(response => { executeRequest(vinyldnsRequest, request.user).map(response => {
@ -431,6 +457,23 @@ class VinylDNS @Inject() (
}) })
} }
def getZoneChange(id: String): 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",
s"zones/$id/changes",
parameters = queryParameters)
executeRequest(vinyldnsRequest, request.user).map(response => {
Status(response.status)(response.body)
.withHeaders(cacheHeaders: _*)
})
}
def syncZone(id: String): Action[AnyContent] = userAction.async { implicit request => def syncZone(id: String): Action[AnyContent] = userAction.async { implicit request =>
// $COVERAGE-OFF$ // $COVERAGE-OFF$
val vinyldnsRequest = val vinyldnsRequest =

View File

@ -2,110 +2,281 @@
@content = { @content = {
<!-- PAGE CONTENT --> <!-- PAGE CONTENT -->
<div class="right_col" role="main" > <div class="right_col" role="main" >
<div> <div>
<!-- START BREADCRUMB --> <!-- START BREADCRUMB -->
<ul class="breadcrumb"> <ul class="breadcrumb">
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li><a href="/groups">Groups</a></li> <li><a href="/groups">Groups</a></li>
<li class="active">{{membership.group.name}}</li> <li class="active">{{membership.group.name}}</li>
</ul> </ul>
<!-- END BREADCRUMB --> <!-- END BREADCRUMB -->
<!-- PAGE TITLE --> <!-- PAGE TITLE -->
<div class="page-title"> <div class="page-title">
<h3><span class="fa fa-group"></span> Group {{membership.group.name}}</h3> <h3><span class="fa fa-group"></span> Group {{membership.group.name}}</h3>
</div>
<!-- END PAGE TITLE -->
<!-- PAGE CONTENT WRAPPER -->
<div class="page-content-wrap">
<div class="alert-wrapper">
<div ng-repeat="alert in alerts">
<notification ng-model="alert"></notification>
</div>
</div> </div>
<!-- END PAGE TITLE -->
<div class="row"> <!-- PAGE CONTENT WRAPPER -->
<div class="col-md-12"> <div class="page-content-wrap">
<p ng-if="membership.group.description"><strong>Description:</strong> {{membership.group.description}}</p>
<p><strong>Group Email:</strong> {{membership.group.email}}</p> <div class="alert-wrapper">
<!-- START SIMPLE DATATABLE --> <div ng-repeat="alert in alerts">
<div class="panel panel-default"> <notification ng-model="alert"></notification>
<div class="panel-heading"> </div>
<div ng-if="isGroupAdmin"> </div>
<form class="form-inline" role="form" name="addMemberForm" ng-submit="addMemberForm.$valid && addMember();">
<div class="col-md-8"> <!-- START VERTICAL TABS -->
<div class="form-group"> <div class="panel panel-default panel-tabs">
<div class="input-group"> <ul class="nav nav-tabs bar_tabs">
<input type="text" ng-model="newMemberData.login" name="newMemberLogin" class="form-control" placeholder="User Name" required/> <li class="active"><a href="#tab1" data-toggle="tab">Manage Groups</a></li>
<li><a href="#tab2" data-toggle="tab">Change History</a></li>
</ul>
<div class="panel-body tab-content">
<div class="tab-pane active" id="tab1">
<div class="row">
<div class="col-md-12">
<p ng-if="membership.group.description"><strong>Description:</strong> {{membership.group.description}}</p>
<p><strong>Group Email:</strong> {{membership.group.email}}</p>
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<div ng-if="isGroupAdmin">
<form class="form-inline" role="form" name="addMemberForm" ng-submit="addMemberForm.$valid && addMember();">
<div class="col-md-8">
<div class="form-group">
<div class="input-group">
<input type="text" ng-model="newMemberData.login" name="newMemberLogin" class="form-control" placeholder="User Name" required/>
</div>
</div>
<div class="form-group" style="padding-left: 10px;">
<label class="check">
<input type="checkbox"
ng-model="newMemberData.isAdmin" name="newMemberAdmin"
class="icheckbox_minimal-grey"/>
Is Group Manager?</label>
</div>
<div class="form-group" style="padding-left: 10px;">
<div class="input-group">
<button type="submit" class="btn btn-sm vinyldns-btn-dark">Add Group Member</button>
</div>
</div>
</div>
</form>
</div> </div>
</div> </div>
<div class="form-group" style="padding-left: 10px;"> <div class="panel-body">
<label class="check"> <p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p>
<input type="checkbox" <p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p>
ng-model="newMemberData.isAdmin" name="newMemberAdmin" <table class="table datatable_simple group-members" ng-if="membership.members.length">
class="icheckbox_minimal-grey"/> <thead>
Is Group Manager?</label> <tr>
</div> <th>User Name</th>
<div class="form-group" style="padding-left: 10px;"> <th>Name</th>
<div class="input-group"> <th>Email</th>
<button type="submit" class="btn btn-sm vinyldns-btn-dark">Add Group Member</button> <th>Group Manager</th>
</div> <th>Status</th>
<th ng-if="isGroupAdmin">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="member in membership.members | orderBy:'+userName'">
<td>{{member.userName}}</td>
<td>{{([member.lastName, member.firstName] | filter: "" ).join(", ")}}</td>
<td>{{member.email}}</td>
<td>
<label class="switch col-md-1">
<input class="switch-checkbox" type="checkbox" ng-model="member.isAdmin" ng-disabled="!isGroupAdmin" ng-change="toggleAdmin(member);"/>
<span class="slider"></span>
</label>
</td>
<td>{{member.lockStatus}}</td>
<td ng-if="isGroupAdmin">
<button class="btn btn-danger btn-rounded" ng-click="removeMember(member.id);">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
</form> <!-- END SIMPLE DATATABLE -->
</div>
</div> </div>
</div> </div>
<div class="panel-body"> <div class="tab-pane" id="tab2">
<p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p> <!-- START SIMPLE DATATABLE -->
<p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p> <div class="panel panel-default">
<table class="table datatable_simple group-members" ng-if="membership.members.length"> <div class="panel-heading">
<thead> <h3 class="panel-title">All Group Changes {{ getChangePageTitle() }}</h3>
<tr> </div>
<th>User Name</th> <div class="panel-body">
<th>Name</th> <div class="btn-group">
<th>Email</th> <button class="btn btn-default" ng-click="refreshGroupChanges()"><span class="fa fa-refresh"></span> Refresh</button>
<th>Group Manager</th> </div>
<th>Status</th>
<th ng-if="isGroupAdmin">Actions</th> <!-- PAGINATION -->
</tr> <div class="dataTables_paginate">
</thead> <ul class="pagination">
<tbody> <li class="paginate_button previous">
<tr ng-repeat="member in membership.members | orderBy:'+userName'"> <a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
<td>{{member.userName}}</td> </li>
<td>{{([member.lastName, member.firstName] | filter: "" ).join(", ")}}</td> <li class="paginate_button next">
<td>{{member.email}}</td> <a ng-if="changeNextPageEnabled()" ng-click="changeNextPage()" class="paginate_button">Next</a>
<td> </li>
<label class="switch col-md-1"> </ul>
<input class="switch-checkbox" type="checkbox" ng-model="member.isAdmin" ng-disabled="!isGroupAdmin" ng-change="toggleAdmin(member);"/> </div>
<span class="slider"></span> <!-- END PAGINATION -->
</label>
</td> <table id="changeDataTable" class="table table-hover table-striped">
<td>{{member.lockStatus}}</td> <thead>
<td ng-if="isGroupAdmin"> <tr>
<button class="btn btn-danger btn-rounded" ng-click="removeMember(member.id);"> <th>Time</th>
Delete <th>Group Change ID</th>
</button> <th>Change Type</th>
</td> <th>Change Message</th>
</tr> <th>Change Info</th>
</tbody> <th>User</th>
</table> </tr>
</thead>
<tbody>
<tr ng-repeat="change in groupChanges track by $index">
<td>{{change.created}}</td>
<td>{{change.id}}</td>
<td>{{change.changeType}}</td>
<td class="col-md-3 wrap-long-text" ng-bind-html="changeMessage(change.groupChangeMessage)"></td>
<td class="col-md-3 wrap-long-text">
<a ng-if="change.changeType =='Create'" ng-click="viewGroupInfo(change.newGroup)" class="force-cursor">View created group</a>
<div><a ng-if="change.changeType =='Update'" ng-click="viewGroupInfo(change.newGroup)" class="force-cursor">View new group</a></div>
<div><a ng-if="change.changeType =='Update'" ng-click="viewGroupInfo(change.oldGroup)" class="force-cursor">View old group</a></div>
</td>
<td>{{change.userName}}</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate">
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="changePrevPageEnabled()" ng-click="changePrevPage()" class="paginate_button">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="changeNextPageEnabled()" ng-click="changeNextPage()" class="paginate_button">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
<div class="panel-footer"></div>
</div>
<!-- END SIMPLE DATATABLE -->
</div> </div>
</div> </div>
<!-- END SIMPLE DATATABLE -->
</div> </div>
</div> <!-- END VERTICAL TABS -->
</div> </div>
<!-- PAGE CONTENT WRAPPER --> <!-- PAGE CONTENT WRAPPER -->
</div>
</div> </div>
<!-- END PAGE CONTENT -->
<form name="viewGroupForm" role="form" class="form-horizontal" novalidate>
<modal modal-id="group_modal" modal-title="{{ groupModal.title }}">
<modal-body>
<modal-element label="Group ID">
<input id="create-group-id-text" type="text" name="groupID" class="form-control"
ng-model="currentGroup.id"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Name">
<input id="create-group-name-text" type="text" name="groupName" class="form-control"
ng-model="currentGroup.name"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Description">
<input id="create-group-description-text" type="text" name="groupDescription" class="form-control"
ng-model="currentGroup.description"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Email">
<input id="create-group-email-text" type="text"
name="groupEmail"
class="form-control"
ng-model="currentGroup.email"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Created">
<input id="create-group-created-text" type="text"
name="groupCreated"
class="form-control"
ng-model="currentGroup.created"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Status">
<input id="create-group-status-text" type="text"
name="groupStatus"
class="form-control"
ng-model="currentGroup.status"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly"/>
</modal-element>
<modal-element label="Group Members IDs (one per line)">
<textarea id="create-group-members-ids-text"
name="groupMembers"
ng-model="currentGroup.memberIds"
rows="5"
class="form-control"
ng-list="&#10;"
ng-trim="false"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly">
</textarea>
</modal-element>
<modal-element label="Group Admins IDs (one per line)">
<textarea id="create-group-admins-ids-text"
name="groupAdmins"
ng-model="currentGroup.adminIds"
rows="5"
class="form-control"
ng-list="&#10;"
ng-trim="false"
ng-class="groupModal.details.class"
ng-readonly="groupModal.details.readOnly">
</textarea>
</modal-element>
</modal-body>
<modal-footer>
<span ng-if="groupModal.action == groupModalState.VIEW_DETAILS">
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeGroupModal()">Close</button>
</span>
</modal-footer>
</modal>
</form>
</div>
<!-- END PAGE CONTENT -->
} }
@plugins = { @plugins = {

View File

@ -26,12 +26,12 @@
<!-- START VERTICAL TABS --> <!-- START VERTICAL TABS -->
<div class="panel panel-default panel-tabs"> <div class="panel panel-default panel-tabs">
<ul class="nav nav-tabs bar_tabs"> <ul class="nav nav-tabs bar_tabs">
<li class="active"><a data-toggle="tab" ng-click="myGroups()">My Groups</a></li> <li class="active"><a href="#myGroups" data-toggle="tab">My Groups</a></li>
<li><a data-toggle="tab" ng-click="allGroups()">All Groups</a></li> <li><a id="tab2-button" href="#allGroups" data-toggle="tab">All Groups</a></li>
</ul> </ul>
<div class="panel-body tab-content"> <div class="panel-body tab-content">
<div class="tab-pane active" id="groups"> <div class="tab-pane active" id="myGroups">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -66,7 +66,22 @@
<div id="group-list" class="panel-body"> <div id="group-list" class="panel-body">
<p ng-if="!groupsLoaded">Loading groups...</p> <p ng-if="!groupsLoaded">Loading groups...</p>
<p ng-if="haveNoGroups(groups.items.length)">You don't have any groups yet.</p> <p ng-if="haveNoGroups(groups.items.length)">You don't have any groups yet.</p>
<p ng-if="searchCriteria(groups.items.length)">No groups match the search criteria.</p> <p ng-if="$scope.groupsLoaded && searchCriteria(groups.items.length)">No groups match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("myGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myGroups')" ng-click="prevPageMyGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myGroups')" ng-click="nextPageMyGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table datatable_simple" ng-if="groups.items.length"> <table class="table datatable_simple" ng-if="groups.items.length">
<thead> <thead>
<tr> <tr>
@ -96,6 +111,118 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("myGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('myGroups')" ng-click="prevPageMyGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('myGroups')" ng-click="nextPageMyGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<!-- END SIMPLE DATATABLE -->
</div>
</div>
</div>
<div class="tab-pane" id="allGroups">
<div class="row">
<div class="col-md-12">
<!-- SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group">
<button id="open-group-modal-button" class="btn btn-default" ng-click="openModal($event);">
<span class="fa fa-plus"></span> New Group
</button>
<button id="refresh-group-button" class="btn btn-default" ng-click="refresh();">
<span class="fa fa-refresh"></span> Refresh
</button>
</div>
<!-- SEARCH BOX -->
<div class="pull-right">
<form class="input-group" ng-submit="refresh()">
<div class="input-group">
<span class="input-group-btn">
<button id="group-search-button" type="submit" class="btn btn-primary btn-left-round">
<span class="fa fa-search"></span>
</button>
</span>
<input id="group-search-text" ng-model="query" type="text" class="form-control" placeholder="Group Name"/>
</div>
</form>
</div>
<!-- END SEARCH BOX -->
</div>
<div id="group-list" class="panel-body">
<p ng-if="!allGroupsLoaded">Loading groups...</p>
<p ng-if="$scope.allGroupsLoaded && searchCriteria(allGroups.items.length)">No groups match the search criteria.</p>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("allGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allGroups')" ng-click="prevPageAllGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allGroups')" ng-click="nextPageAllGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
<table class="table datatable_simple" ng-if="allGroup.items.length">
<thead>
<tr>
<th>Group Name</th>
<th>Email</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="group in allGroup.items | orderBy:'+name'">
<td class="wrap-long-text">
<a ng-href="/groups/{{group.id}}">{{group.name}}</a>
</td>
<td class="wrap-long-text">{{group.email}}</td>
<td class="wrap-long-text">{{group.description}}</td>
<td>
<div class="table-form-group">
<a class="btn btn-info btn-rounded" ng-href="/groups/{{group.id}}">
View</a>
<a ng-if="groupAdmin(group)" class="btn btn-warning btn-rounded" ng-click="editGroup(group);">
Edit</a>
<button ng-if="groupAdmin(group)" id="delete-group-{{group.name}}" class="btn btn-danger btn-rounded" ng-click="confirmDeleteGroup(group);">
Delete</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_page_number">{{ getGroupsPageNumber("allGroups") }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled('allGroups')" ng-click="prevPageAllGroups()">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled('allGroups')" ng-click="nextPageAllGroups()">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div> </div>
</div> </div>
<!-- END SIMPLE DATATABLE --> <!-- END SIMPLE DATATABLE -->

View File

@ -41,7 +41,8 @@
<ul class="nav nav-tabs bar_tabs"> <ul class="nav nav-tabs bar_tabs">
<li class="active"><a href="#tab1" data-toggle="tab">Manage Records</a></li> <li class="active"><a href="#tab1" data-toggle="tab">Manage Records</a></li>
<li><a id="tab2-button" href="#tab2" data-toggle="tab">Manage Zone</a></li> <li><a id="tab2-button" href="#tab2" data-toggle="tab">Manage Zone</a></li>
<li><a href="#tab3" data-toggle="tab">Change History</a></li> <li><a href="#tab3" data-toggle="tab">Record Change History</a></li>
<li><a href="#tab4" data-toggle="tab">Zone Change History</a></li>
</ul> </ul>
<div class="panel-body tab-content"> <div class="panel-body tab-content">
@ -54,6 +55,9 @@
<div class="tab-pane" id="tab3"> <div class="tab-pane" id="tab3">
@changeHistory(request) @changeHistory(request)
</div> </div>
<div class="tab-pane" id="tab4" ng-controller="ManageZonesController">
@zoneChangeHistory(request)
</div>
</div> </div>
</div> </div>
<!-- END VERTICAL TABS --> <!-- END VERTICAL TABS -->

View File

@ -0,0 +1,121 @@
@(implicit request: play.api.mvc.Request[Any])
<!-- START SIMPLE DATATABLE -->
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Zone Change History</h3>
</div>
<div class="panel-body">
<div class="btn-group">
<button class="btn btn-default" ng-click="refreshZoneChange()"><span class="fa fa-refresh"></span> Refresh</button>
</div>
<table id="zoneChangeDataTable" class="table table-hover table-striped">
<thead>
<tr>
<th>User name</th>
<th>Email</th>
<th>Access</th>
<th>Created</th>
<th>Updated</th>
<th>Change type</th>
<th>Admin group</th>
<th>ACL</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="zoneChange in zoneChanges track by $index">
<td>{{ zoneChange.userName }}</td>
<td>{{ zoneChange.zone.email }}</td>
<td>{{ zoneChange.zone.shared ? "Shared" : "Private" }}</td>
<td>{{ zoneChange.zone.created }}</td>
<td>{{ zoneChange.zone.updated }}</td>
<td>{{ zoneChange.changeType }}</td>
<td><a ng-bind="zoneChange.zone.adminGroupName" href="/groups/{{zoneChange.zone.adminGroupId}}"></a>
</td>
<td>
<button class="btn btn-info btn-sm"
ng-if="zoneChange.zone.acl.rules.length != 0"
ng-click="refreshAclRule($index)">ACL Rules
</button>
</td>
</tr>
</tbody>
</table>
<!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate">
<span class="vinyldns_zones_page_number">{{ getZoneHistoryPageNumber() }}</span>
<ul class="pagination">
<li class="paginate_button previous">
<a ng-if="prevPageEnabled()" ng-click="prevPageZoneHistory()" class="paginate_button">Previous</a>
</li>
<li class="paginate_button next">
<a ng-if="nextPageEnabled()" ng-click="nextPageZoneHistory()" class="paginate_button">Next</a>
</li>
</ul>
</div>
<!-- END PAGINATION -->
</div>
</div>
<div class="panel-footer"></div>
<!-- END SIMPLE DATATABLE -->
<!-- THE ACL RULE MODAL FORM STARTS -->
<form name="aclModalViewForm" role="form" class="form-horizontal" novalidate>
<modal modal-id="aclModalView" modal-title="{{ aclRulesModal.title }}">
<modal-body>
<table id="aclRuleTable" class="table table-hover table-striped">
<thead>
<tr>
<th>User/Group</th>
<th>Access Level</th>
<th>Record Types</th>
<th>Record Mask</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="rule in allAclRules track by $index">
<td class="wrap-long-text">
<a ng-if="rule.groupId != undefined" href="/groups/{{rule.groupId}}">
{{rule.groupName}}
</a>
<span ng-if="rule.groupId == undefined">
{{rule.userName}}
</span>
</td>
<td>
{{rule.accessLevel}}
</td>
<td>
<span ng-if="rule.recordTypes.length == 0">All Types</span>
<ul class="table-cell-list">
<li ng-repeat="item in rule.recordTypes">
{{item}}
</li>
</ul>
</td>
<td class="wrap-long-text">
{{rule.recordMask}}
</td>
<td class="wrap-long-text">
{{rule.description}}
</td>
</tr>
</tbody>
</table>
</modal-body>
<modal-footer>
<span>
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="closeAclModalView()">Close</button>
</span>
</modal-footer>
</modal>
</form>
<!-- THE ACL RULE MODAL FORM ENDS -->

View File

@ -77,8 +77,8 @@
<p ng-if="hasZones && zonesLoaded && !zones.length">No zones match the search criteria.</p> <p ng-if="hasZones && zonesLoaded && !zones.length">No zones match the search criteria.</p>
<!-- PAGINATION --> <!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate"> <div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span> <span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
<ul class="pagination"> <ul class="pagination">
<li class="paginate_button previous"> <li class="paginate_button previous">
<a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a> <a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a>
@ -128,8 +128,8 @@
</table> </table>
<!-- PAGINATION --> <!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate"> <div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span> <span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
<ul class="pagination"> <ul class="pagination">
<li class="paginate_button previous"> <li class="paginate_button previous">
<a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a> <a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a>
@ -191,8 +191,8 @@
<p ng-if="allZonesLoaded && !allZones.length">No zones match the search criteria.</p> <p ng-if="allZonesLoaded && !allZones.length">No zones match the search criteria.</p>
<!-- PAGINATION --> <!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate"> <div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span> <span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
<ul class="pagination"> <ul class="pagination">
<li class="paginate_button previous"> <li class="paginate_button previous">
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a> <a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>
@ -245,8 +245,8 @@
</table> </table>
<!-- PAGINATION --> <!-- PAGINATION -->
<div class="dataTables_paginate vinyldns_zones_paginate"> <div class="dataTables_paginate vinyldns_paginate">
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span> <span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
<ul class="pagination"> <ul class="pagination">
<li class="paginate_button previous"> <li class="paginate_button previous">
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a> <a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>

View File

@ -30,6 +30,7 @@ GET /api/zones @controllers.VinylDNS.getZones
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
GET /api/zones/:id @controllers.VinylDNS.getZone(id: String) GET /api/zones/:id @controllers.VinylDNS.getZone(id: String)
GET /api/zones/name/:name @controllers.VinylDNS.getZoneByName(name: String) GET /api/zones/name/:name @controllers.VinylDNS.getZoneByName(name: String)
GET /api/zones/:id/changes @controllers.VinylDNS.getZoneChange(id: String)
POST /api/zones @controllers.VinylDNS.addZone POST /api/zones @controllers.VinylDNS.addZone
PUT /api/zones/:id @controllers.VinylDNS.updateZone(id: String) PUT /api/zones/:id @controllers.VinylDNS.updateZone(id: String)
DELETE /api/zones/:id @controllers.VinylDNS.deleteZone(id: String) DELETE /api/zones/:id @controllers.VinylDNS.deleteZone(id: String)
@ -44,6 +45,8 @@ GET /api/zones/:id/recordsetchanges @controllers.VinylDNS.listRecor
GET /api/groups @controllers.VinylDNS.getGroups GET /api/groups @controllers.VinylDNS.getGroups
GET /api/groups/:gid @controllers.VinylDNS.getGroup(gid: String) GET /api/groups/:gid @controllers.VinylDNS.getGroup(gid: String)
GET /api/groups/:gid/groupchanges @controllers.VinylDNS.listGroupChanges(gid: String)
GET /api/groups/change/:gcid @controllers.VinylDNS.getGroupChange(gcid: String)
POST /api/groups @controllers.VinylDNS.newGroup POST /api/groups @controllers.VinylDNS.newGroup
PUT /api/groups/:gid @controllers.VinylDNS.updateGroup(gid: String) PUT /api/groups/:gid @controllers.VinylDNS.updateGroup(gid: String)
DELETE /api/groups/:gid @controllers.VinylDNS.deleteGroup(gid: String) DELETE /api/groups/:gid @controllers.VinylDNS.deleteGroup(gid: String)

View File

@ -434,12 +434,12 @@ input[type="file"] {
vertical-align: top; vertical-align: top;
} }
.vinyldns_zones_paginate { .vinyldns_paginate {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.vinyldns_zones_page_number { .vinyldns_page_number {
margin-right: 10px; margin-right: 10px;
} }

View File

@ -14,19 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
angular.module('controller.groups', []).controller('GroupsController', function ($scope, $log, $location, groupsService, profileService, utilityService) { angular.module('controller.groups', []).controller('GroupsController', function ($scope, $log, $location, groupsService, profileService, utilityService, pagingService, $timeout) {
//registering bootstrap modal close event to refresh data after create group action //registering bootstrap modal close event to refresh data after create group action
angular.element('#modal_new_group').one('hide.bs.modal', function () { angular.element('#modal_new_group').one('hide.bs.modal', function () {
$scope.closeModal(); $scope.closeModal();
}); });
$scope.groups = {items: []}; $scope.groups = {items: []};
$scope.allGroup = {items: []};
$scope.groupsLoaded = false; $scope.groupsLoaded = false;
$scope.allGroupsLoaded = false;
$scope.alerts = []; $scope.alerts = [];
$scope.ignoreAccess = false; $scope.ignoreAccess = false;
$scope.hasGroups = false; // Re-assigned each time groups are fetched without a query $scope.hasGroups = false;
$scope.query = ""; $scope.query = "";
// Paging status for group sets
var groupsPaging = pagingService.getNewPagingParams(100);
var allGroupsPaging = pagingService.getNewPagingParams(100);
function handleError(error, type) { function handleError(error, type) {
var alert = utilityService.failure(error, type); var alert = utilityService.failure(error, type);
$scope.alerts.push(alert); $scope.alerts.push(alert);
@ -68,7 +74,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
$("#group-search-text").autocomplete({ $("#group-search-text").autocomplete({
source: function( request, response ) { source: function( request, response ) {
$.ajax({ $.ajax({
url: "/api/groups?maxItems=1500&abridged=true", url: "/api/groups?maxItems=100&abridged=true",
dataType: "json", dataType: "json",
data: {groupNameFilter: request.term, ignoreAccess: $scope.ignoreAccess}, data: {groupNameFilter: request.term, ignoreAccess: $scope.ignoreAccess},
success: function(data) { success: function(data) {
@ -142,30 +148,33 @@ angular.module('controller.groups', []).controller('GroupsController', function
}); });
}; };
$scope.allGroups = function () {
$scope.ignoreAccess = true;
$scope.refresh();
}
$scope.myGroups = function () {
$scope.ignoreAccess = false;
$scope.refresh();
}
$scope.refresh = function () { $scope.refresh = function () {
function success(result) { groupsPaging = pagingService.resetPaging(groupsPaging);
$log.log('getGroups:refresh-success', result); allGroupsPaging = pagingService.resetPaging(allGroupsPaging);
//update groups
$scope.groups.items = result.groups;
$scope.groupsLoaded = true;
if (!$scope.query.length) {
$scope.hasGroups = $scope.groups.items.length > 0;
}
return result;
}
getGroupsAbridged($scope.ignoreAccess) groupsService
.then(success) .getGroupsAbridged(groupsPaging.maxItems, undefined, false, $scope.query)
.then(function (result) {
$log.debug('getGroups:refresh-success', result);
//update groups
groupsPaging.next = result.data.nextId;
updateGroupDisplay(result.data.groups);
if (!$scope.query.length) {
$scope.hasGroups = $scope.groups.items.length > 0;
}
})
.catch(function (error) {
handleError(error, 'getGroups::refresh-failure');
});
groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, undefined, true, $scope.query)
.then(function (result) {
$log.debug('getGroups:refresh-success', result);
//update groups
allGroupsPaging.next = result.data.nextId;
updateAllGroupDisplay(result.data.groups);
})
.catch(function (error) { .catch(function (error) {
handleError(error, 'getGroups::refresh-failure'); handleError(error, 'getGroups::refresh-failure');
}); });
@ -199,20 +208,6 @@ angular.module('controller.groups', []).controller('GroupsController', function
}); });
} }
function getGroupsAbridged() {
function success(response) {
$log.log('groupsService::getGroups-success');
return response.data;
}
return groupsService
.getGroupsAbridged($scope.ignoreAccess, $scope.query)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getGroups-failure');
});
}
// Return true if there are no groups created by the user // Return true if there are no groups created by the user
$scope.haveNoGroups = function (groupLength) { $scope.haveNoGroups = function (groupLength) {
if (!$scope.hasGroups && !groupLength && $scope.groupsLoaded && $scope.query.length == "") { if (!$scope.hasGroups && !groupLength && $scope.groupsLoaded && $scope.query.length == "") {
@ -224,7 +219,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
// Return true if no groups are found related to the search query // Return true if no groups are found related to the search query
$scope.searchCriteria = function (groupLength) { $scope.searchCriteria = function (groupLength) {
if ($scope.groupsLoaded && !groupLength && $scope.query.length != "") { if (!groupLength && $scope.query.length != "") {
return true return true
} else { } else {
return false return false
@ -336,4 +331,115 @@ angular.module('controller.groups', []).controller('GroupsController', function
.then(profileSuccess, profileFailure) .then(profileSuccess, profileFailure)
.catch(profileFailure); .catch(profileFailure);
function updateGroupDisplay (groups) {
$scope.groups.items = groups;
$scope.groupsLoaded = true;
$log.debug("Displaying my groups: ", $scope.groups.items);
if($scope.groups.items.length > 0) {
$("td.dataTables_empty").hide();
} else {
$("td.dataTables_empty").show();
}
}
function updateAllGroupDisplay (groups) {
$scope.allGroup.items = groups;
$scope.allGroupsLoaded = true;
$log.debug("Displaying all groups: ", $scope.allGroup.items);
if($scope.allGroup.items.length > 0) {
$("td.dataTables_empty").hide();
} else {
$("td.dataTables_empty").show();
}
}
/*
* Group set paging
*/
$scope.getGroupsPageNumber = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.getPanelTitle(groupsPaging);
case 'allGroups':
return pagingService.getPanelTitle(allGroupsPaging);
}
};
$scope.prevPageEnabled = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.prevPageEnabled(groupsPaging);
case 'allGroups':
return pagingService.prevPageEnabled(allGroupsPaging);
}
};
$scope.nextPageEnabled = function(tab) {
switch(tab) {
case 'myGroups':
return pagingService.nextPageEnabled(groupsPaging);
case 'allGroups':
return pagingService.nextPageEnabled(allGroupsPaging);
}
};
$scope.prevPageMyGroups = function() {
var startFrom = pagingService.getPrevStartFrom(groupsPaging);
return groupsService
.getGroupsAbridged(groupsPaging.maxItems, startFrom, false, $scope.query)
.then(function(response) {
groupsPaging = pagingService.prevPageUpdate(response.data.nextId, groupsPaging);
updateGroupDisplay(response.data.groups);
})
.catch(function (error) {
handleError(error,'groupsService::prevPageMyGroups-failure');
});
}
$scope.prevPageAllGroups = function() {
var startFrom = pagingService.getPrevStartFrom(allGroupsPaging);
return groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, startFrom, true, $scope.query)
.then(function(response) {
allGroupsPaging = pagingService.prevPageUpdate(response.data.nextId, allGroupsPaging);
updateAllGroupDisplay(response.data.groups);
})
.catch(function (error) {
handleError(error,'groupsService::prevPageAllGroups-failure');
});
}
$scope.nextPageMyGroups = function () {
return groupsService
.getGroupsAbridged(groupsPaging.maxItems, groupsPaging.next, false, $scope.query)
.then(function(response) {
var groupSets = response.data.groups;
groupsPaging = pagingService.nextPageUpdate(groupSets, response.data.nextId, groupsPaging);
if (groupSets.length > 0) {
updateGroupDisplay(response.data.groups);
}
})
.catch(function (error) {
handleError(error,'groupsService::nextPageMyGroups-failure')
});
};
$scope.nextPageAllGroups = function () {
return groupsService
.getGroupsAbridged(allGroupsPaging.maxItems, allGroupsPaging.next, true, $scope.query)
.then(function(response) {
var groupSets = response.data.groups;
allGroupsPaging = pagingService.nextPageUpdate(groupSets, response.data.nextId, allGroupsPaging);
if (groupSets.length > 0) {
updateAllGroupDisplay(response.data.groups);
}
})
.catch(function (error) {
handleError(error,'groupsService::nextPageAllGroups-failure')
});
};
$timeout($scope.refresh, 0);
}); });

View File

@ -19,14 +19,16 @@ describe('Controller: GroupsController', function () {
module('ngMock'), module('ngMock'),
module('service.groups'), module('service.groups'),
module('service.profile'), module('service.profile'),
module('service.utility') module('service.utility'),
module('service.paging'),
module('controller.groups') module('controller.groups')
}); });
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService) { beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService, pagingService) {
this.scope = $rootScope.$new(); this.scope = $rootScope.$new();
this.groupsService = groupsService; this.groupsService = groupsService;
this.utilityService = utilityService; this.utilityService = utilityService;
this.q = $q; this.q = $q;
this.pagingService = pagingService;
profileService.getAuthenticatedUserData = function() { profileService.getAuthenticatedUserData = function() {
return $q.when('data') return $q.when('data')
@ -38,6 +40,15 @@ describe('Controller: GroupsController', function () {
} }
}) })
}; };
groupsService.getGroupsAbridged = function () {
return $q.when({
data: {
groups: ["all my groups"]
}
});
};
this.controller = $controller('GroupsController', {'$scope': this.scope}); this.controller = $controller('GroupsController', {'$scope': this.scope});
this.mockSuccessAlert = 'success'; this.mockSuccessAlert = 'success';
@ -64,7 +75,7 @@ describe('Controller: GroupsController', function () {
this.scope.refresh(); this.scope.refresh();
this.scope.$digest(); this.scope.$digest();
expect(getGroups.calls.count()).toBe(1); expect(getGroups.calls.count()).toBe(2);
expect(this.scope.groups.items).toBe("all my groups"); expect(this.scope.groups.items).toBe("all my groups");
}); });
@ -111,4 +122,110 @@ describe('Controller: GroupsController', function () {
expect(this.utilityFailure.calls.count()).toBe(1); expect(this.utilityFailure.calls.count()).toBe(1);
expect(this.scope.alerts).toEqual([this.mockFailureAlert]); expect(this.scope.alerts).toEqual([this.mockFailureAlert]);
}); });
it('nextPageMyGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all my groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = false;
this.scope.nextPageMyGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('prevPageMyGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all my groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = false;
this.scope.prevPageMyGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
this.scope.nextPageMyGroups();
this.scope.prevPageMyGroups();
expect(getGroupSets.calls.count()).toBe(3);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('nextPageAllGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = true;
this.scope.nextPageAllGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
it('prevPageAllGroups should call getGroupsAbridged with the correct parameters', function () {
var response = {
data: {
groups: "all groups"
}
};
var getGroupSets = spyOn(this.groupsService, 'getGroupsAbridged')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedQuery = this.scope.query;
var expectedIgnoreAccess = true;
this.scope.prevPageAllGroups();
expect(getGroupSets.calls.count()).toBe(1);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
this.scope.nextPageAllGroups();
this.scope.prevPageAllGroups();
expect(getGroupSets.calls.count()).toBe(3);
expect(getGroupSets.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedIgnoreAccess, expectedQuery]);
});
}); });

View File

@ -16,7 +16,7 @@
angular.module('controller.manageZones', []) angular.module('controller.manageZones', [])
.controller('ManageZonesController', function ($scope, $timeout, $log, recordsService, zonesService, groupsService, .controller('ManageZonesController', function ($scope, $timeout, $log, recordsService, zonesService, groupsService,
profileService, utilityService) { profileService, utilityService, pagingService) {
groupsService.getGroupsStored() groupsService.getGroupsStored()
.then(function (results) { .then(function (results) {
@ -38,6 +38,7 @@ angular.module('controller.manageZones', [])
$scope.alerts = []; $scope.alerts = [];
$scope.zoneInfo = {}; $scope.zoneInfo = {};
$scope.zoneChanges = {};
$scope.updateZoneInfo = {}; $scope.updateZoneInfo = {};
$scope.manageZoneState = { $scope.manageZoneState = {
UPDATE: 0, UPDATE: 0,
@ -60,7 +61,8 @@ angular.module('controller.manageZones', [])
CREATE: 0, CREATE: 0,
UPDATE: 1, UPDATE: 1,
CONFIRM_UPDATE: 2, CONFIRM_UPDATE: 2,
CONFIRM_DELETE: 3 CONFIRM_DELETE: 3,
VIEW_DETAILS: 4
}; };
$scope.aclModalParams = { $scope.aclModalParams = {
readOnly: { readOnly: {
@ -74,6 +76,8 @@ angular.module('controller.manageZones', [])
}; };
$scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT']; $scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
var zoneHistoryPaging = pagingService.getNewPagingParams(100);
/** /**
* Zone modal control functions * Zone modal control functions
*/ */
@ -276,6 +280,7 @@ angular.module('controller.manageZones', [])
$scope.updateZoneInfo.hiddenTransferKey = ''; $scope.updateZoneInfo.hiddenTransferKey = '';
$scope.currentManageZoneState = $scope.manageZoneState.UPDATE; $scope.currentManageZoneState = $scope.manageZoneState.UPDATE;
$scope.refreshAclRuleDisplay(); $scope.refreshAclRuleDisplay();
$scope.refreshZoneChange();
} }
return recordsService return recordsService
.getZone($scope.zoneId) .getZone($scope.zoneId)
@ -285,6 +290,53 @@ angular.module('controller.manageZones', [])
}); });
}; };
$scope.refreshZoneChange = function() {
zoneHistoryPaging = pagingService.resetPaging(zoneHistoryPaging);
function success(response) {
$log.log('zonesService::getZoneChanges-success');
zoneHistoryPaging.next = response.data.nextId;
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges);
}
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, undefined, $scope.zoneId)
.then(success)
.catch(function (error) {
handleError(error, 'zonesService::getZoneChanges-failure');
});
};
$scope.refreshAclRule = function (index) {
$scope.allAclRules = [];
$scope.aclRulesModal = {
action: $scope.aclModalState.VIEW_DETAILS,
title: "ACL Rules Info",
basics: $scope.aclModalParams.readOnly,
details: $scope.aclModalParams.readOnly,
};
if ($scope.zoneChanges[index].zone.acl.rules.length!=0){
for (var length = 0; length < $scope.zoneChanges[index].zone.acl.rules.length; length++) {
$scope.allAclRules.push($scope.zoneChanges[index].zone.acl.rules[length]);
if ($scope.allAclRules[length].hasOwnProperty('userId')){
getAclUser($scope.allAclRules[length].userId, length); }
else{ getAclGroup($scope.allAclRules[length].groupId, length);}
}
$scope.aclModalViewForm.$setPristine();
$("#aclModalView").modal("show");}
else{$("#aclModalView").modal("hide");}
};
$scope.closeAclModalView = function() {
$scope.aclModalViewForm.$setPristine();
};
$scope.updateZoneChangeDisplay = function (zoneChange) {
for (var length = 0; length < zoneChange.length; length++) {
getZoneGroup(zoneChange[length].zone.adminGroupId, length);
getZoneUser(zoneChange[length].userId, length);
}
};
$scope.refreshAclRuleDisplay = function() { $scope.refreshAclRuleDisplay = function() {
$scope.aclRules = []; $scope.aclRules = [];
angular.forEach($scope.zoneInfo.acl.rules, function (rule) { angular.forEach($scope.zoneInfo.acl.rules, function (rule) {
@ -292,6 +344,109 @@ angular.module('controller.manageZones', [])
}); });
}; };
/**
* Get User name and Group Name with Ids for Zone history
*/
function getZoneGroup(groupId, length) {
function success(response) {
$log.log('groupsService::getZoneGroup-success');
$scope.zoneChanges[length].zone.adminGroupName = response.data.name;
}
return groupsService
.getGroup(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getZoneGroup-failure');
});
}
function getZoneUser(userId, length) {
function success(response) {
$log.log('profileService::getZoneUserDataById-success');
$scope.zoneChanges[length].userName = response.data.userName;
}
return profileService
.getUserDataById(userId)
.then(success)
.catch(function (error) {
handleError(error, 'profileService::getZoneUserDataById-failure');
});
};
function getAclGroup(groupId, length) {
function success(response) {
$log.log('groupsService::getAclGroup-success');
$scope.allAclRules[length].groupName = response.data.name;
}
return groupsService
.getGroup(groupId)
.then(success)
.catch(function (error) {
handleError(error, 'groupsService::getAclGroup-failure');
});
}
function getAclUser(userId, length) {
function success(response) {
$log.log('profileService::getAclUserDataById-success');
$scope.allAclRules[length].userName = response.data.userName;
}
return profileService
.getUserDataById(userId)
.then(success)
.catch(function (error) {
handleError(error, 'profileService::getAclUserDataById-failure');
});
};
/**
* Zone history Pagination
*/
$scope.getZoneHistoryPageNumber = function() {
return pagingService.getPanelTitle(zoneHistoryPaging);
};
$scope.prevPageEnabled = function() {
return pagingService.prevPageEnabled(zoneHistoryPaging);
};
$scope.nextPageEnabled = function(tab) {
return pagingService.nextPageEnabled(zoneHistoryPaging);
};
$scope.nextPageZoneHistory = function () {
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, zoneHistoryPaging.next, $scope.zoneId )
.then(function(response) {
var zoneChanges = response.data.zoneChanges;
zoneHistoryPaging = pagingService.nextPageUpdate(zoneChanges, response.data.nextId, zoneHistoryPaging);
if (zoneChanges.length > 0) {
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges)
}
})
.catch(function (error) {
handleError(error,'zonesService::nextPage-failure')
});
};
$scope.prevPageZoneHistory = function() {
var startFrom = pagingService.getPrevStartFrom(zoneHistoryPaging);
return zonesService
.getZoneChanges(zoneHistoryPaging.maxItems, startFrom, $scope.zoneId )
.then(function(response) {
zoneHistoryPaging = pagingService.prevPageUpdate(response.data.nextId, zoneHistoryPaging);
$scope.zoneChanges = response.data.zoneChanges;
$scope.updateZoneChangeDisplay(response.data.zoneChanges);
})
.catch(function (error) {
handleError(error,'zonesService::prevPage-failure');
});
};
/** /**
* Service interaction functions * Service interaction functions
*/ */

View File

@ -22,16 +22,18 @@ describe('Controller: ManageZonesController', function () {
module('service.utility'), module('service.utility'),
module('service.zones'), module('service.zones'),
module('service.profile'), module('service.profile'),
module('service.paging'),
module('controller.manageZones') module('controller.manageZones')
}); });
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, recordsService, zonesService, beforeEach(inject(function ($rootScope, $controller, $q, groupsService, recordsService, zonesService,
profileService) { profileService, pagingService) {
this.rootScope = $rootScope; this.rootScope = $rootScope;
this.scope = $rootScope.$new(); this.scope = $rootScope.$new();
this.groupsService = groupsService; this.groupsService = groupsService;
this.zonesService = zonesService; this.zonesService = zonesService;
this.recordsService = recordsService; this.recordsService = recordsService;
this.profileService = profileService; this.profileService = profileService;
this.pagingService = pagingService;
this.q = $q; this.q = $q;
this.groupsService.getGroups = function () { this.groupsService.getGroups = function () {
return $q.when({ return $q.when({
@ -205,11 +207,14 @@ describe('Controller: ManageZonesController', function () {
} }
} }
}; };
var getZone = spyOn(this.recordsService, 'getZone') var getZone = spyOn(this.recordsService, 'getZone')
.and.stub() .and.stub()
.and.returnValue(this.q.when(mockResponse)); .and.returnValue(this.q.when(mockResponse));
var refreshAclRuleDisplay = spyOn(this.scope, 'refreshAclRuleDisplay') var refreshAclRuleDisplay = spyOn(this.scope, 'refreshAclRuleDisplay')
.and.stub(); .and.stub();
var refreshZoneChange = spyOn(this.scope, 'refreshZoneChange')
.and.stub();
this.scope.currentManageZoneState = this.scope.manageZoneState.CONFIRM_UPDATE; this.scope.currentManageZoneState = this.scope.manageZoneState.CONFIRM_UPDATE;
this.scope.updateZoneInfo.hiddenKey = 'some key'; this.scope.updateZoneInfo.hiddenKey = 'some key';
this.scope.updateZoneInfo.hiddenTransferKey = 'some key'; this.scope.updateZoneInfo.hiddenTransferKey = 'some key';
@ -217,6 +222,7 @@ describe('Controller: ManageZonesController', function () {
this.scope.$digest(); this.scope.$digest();
expect(getZone.calls.count()).toBe(1); expect(getZone.calls.count()).toBe(1);
expect(refreshAclRuleDisplay.calls.count()).toBe(1); expect(refreshAclRuleDisplay.calls.count()).toBe(1);
expect(refreshZoneChange.calls.count()).toBe(1);
expect(this.scope.zoneInfo).toEqual(mockResponse.data.zone); expect(this.scope.zoneInfo).toEqual(mockResponse.data.zone);
expect(this.scope.updateZoneInfo. adminGroupId).toEqual('id101112'); expect(this.scope.updateZoneInfo. adminGroupId).toEqual('id101112');
expect(this.scope.updateZoneInfo.hiddenKey).toEqual(''); expect(this.scope.updateZoneInfo.hiddenKey).toEqual('');
@ -554,4 +560,99 @@ describe('Controller: ManageZonesController', function () {
expect(toDisplayAclRule.calls.count()).toBe(3); expect(toDisplayAclRule.calls.count()).toBe(3);
expect(this.scope.aclRules).toEqual(this.scope.zoneInfo.acl.rules); expect(this.scope.aclRules).toEqual(this.scope.zoneInfo.acl.rules);
}); });
it('next page should call listZoneChangesByZoneId with the correct parameters', function () {
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedZoneId = this.scope.zoneId;
this.scope.nextPageZoneHistory();
expect(getZoneChanges.calls.count()).toBe(1);
expect(getZoneChanges.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedZoneId]);
});
it('prev page should call getZoneChanges with the correct parameters', function () {
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
var expectedZoneId = this.scope.zoneId;
this.scope.prevPageZoneHistory();
expect(getZoneChanges.calls.count()).toBe(1);
expect(getZoneChanges.calls.mostRecent().args).toEqual(
[expectedMaxItems, expectedStartFrom, expectedZoneId]);
});
it('test that we properly get Zone History data', function(){
this.scope.zoneChanges = {};
var mockZoneChange = {data: {
zoneId: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
zoneChanges: [{ zone: {
name: "dummy.",
email: "test@test.com",
status: "Active",
created: "2017-02-15T14:58:39Z",
account: "c8234503-bfda-4b80-897f-d74129051eaa",
acl: {rules: []},
adminGroupId: "c8234503-bfda-4b80-897f-d74129051eaa",
id: "c5c87405-2ec8-4e03-b2dc-c6758a5d9666",
shared: false,
status: "Active",
latestSync: "2017-02-15T14:58:39Z",
isTest: true
}}],maxItems: 100}};
var updateZoneChangeDisplay = spyOn(this.scope, 'updateZoneChangeDisplay')
.and.stub();
var getZoneChanges = spyOn(this.zonesService, 'getZoneChanges')
.and.stub()
.and.returnValue(this.q.when(mockZoneChange));
this.scope.refreshZoneChange();
this.scope.$digest();
expect(getZoneChanges.calls.count()).toBe(1);
expect(this.scope.zoneChanges).toEqual(mockZoneChange.data.zoneChanges);
});
}); });

View File

@ -14,13 +14,35 @@
* limitations under the License. * limitations under the License.
*/ */
angular.module('controller.membership', []).controller('MembershipController', function ($scope, $log, $location, $timeout, angular.module('controller.membership', []).controller('MembershipController', function ($scope, $log, $location, $sce, $timeout, pagingService,
groupsService, profileService, utilityService) { groupsService, profileService, utilityService) {
$scope.membership = { members: [], group: {} }; $scope.membership = { members: [], group: {} };
$scope.membershipLoaded = false; $scope.membershipLoaded = false;
$scope.alerts = []; $scope.alerts = [];
$scope.isGroupAdmin = false; $scope.isGroupAdmin = false;
$scope.groupChanges = {};
$scope.currentGroup = {};
$scope.groupModalState = {
VIEW_DETAILS: 1
};
// read-only data for setting various classes/attributes in group modal
$scope.groupModalParams = {
readOnly: {
class: "",
readOnly: true
}
};
$scope.changeMessage = function (groupChangeMessage) {
message = groupChangeMessage.replaceAll('. ', '.<br>')
return $sce.trustAsHtml(message);
};
// paging status for group changes
var changePaging = pagingService.getNewPagingParams(100);
function handleError(error, type) { function handleError(error, type) {
var alert = utilityService.failure(error, type); var alert = utilityService.failure(error, type);
@ -207,8 +229,113 @@ angular.module('controller.membership', []).controller('MembershipController', f
$scope.resetNewMemberData(); $scope.resetNewMemberData();
$scope.getGroupInfo(id); $scope.getGroupInfo(id);
$scope.refreshGroupChanges(id);
}; };
$scope.refreshGroupChanges = function(id) {
if(!id){
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
}
$log.debug('refreshGroupChanges, loading group with id ', id);
changePaging = pagingService.resetPaging(changePaging);
function success(response) {
$log.debug('groupsService::getGroupChanges-success');
changePaging.next = response.data.nextId;
updateChangeDisplay(response.data.changes)
}
return groupsService
.getGroupChanges(id, changePaging.maxItems, undefined)
.then(success)
.catch(function (error){
handleError(error, 'groupsService::getGroupChanges-failure');
});
};
function updateChangeDisplay(changes) {
var newChanges = [];
angular.forEach(changes, function(change) {
newChanges.push(change);
});
$scope.groupChanges = newChanges;
}
/**
* Group change paging
*/
$scope.getChangePageTitle = function() {
return pagingService.getPanelTitle(changePaging);
};
$scope.changePrevPageEnabled = function() {
return pagingService.prevPageEnabled(changePaging);
};
$scope.changeNextPageEnabled = function() {
return pagingService.nextPageEnabled(changePaging);
};
$scope.changePrevPage = function() {
var startFrom = pagingService.getPrevStartFrom(changePaging);
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
$log.debug('changePrevPage, loading group with id ', id);
return groupsService
.getGroupChanges(id, changePaging.maxItems, startFrom)
.then(function(response) {
changePaging = pagingService.prevPageUpdate(response.data.nextId, changePaging);
updateChangeDisplay(response.data.changes);
})
.catch(function (error) {
handleError(error, 'groupsService::changePrevPage-failure');
});
};
$scope.changeNextPage = function() {
var id = $location.absUrl().toString();
id = id.substring(id.lastIndexOf('/') + 1);
id = id.substring(0, id.indexOf('#'))
$log.debug('changeNextPage, loading group with id ', id);
return groupsService
.getGroupChanges(id, changePaging.maxItems, changePaging.next)
.then(function(response) {
var changes = response.data.changes;
changePaging = pagingService.nextPageUpdate(changes, response.data.nextId, changePaging);
if(changes.length > 0 ){
updateChangeDisplay(changes);
}
})
.catch(function (error) {
handleError(error, 'groupsService::changeNextPage-failure');
});
};
$scope.viewGroupInfo = function(group) {
var newGroup = angular.copy(group);
newGroup.adminIds = [];
angular.forEach(group.admins, function(admin) {
newGroup.adminIds.push(admin.id);
});
newGroup.memberIds = [];
angular.forEach(group.members, function(member) {
newGroup.memberIds.push(member.id);
});
$scope.currentGroup = newGroup;
$scope.groupModal = {
action: $scope.groupModalState.VIEW_DETAILS,
title: "Group Info",
basics: $scope.groupModalParams.readOnly,
details: $scope.groupModalParams.readOnly,
};
$("#group_modal").modal("show");
};
$scope.closeGroupModal = function() {
$scope.viewGroupForm.$setPristine();
};
$timeout($scope.refresh, 0); $timeout($scope.refresh, 0);
}); });

View File

@ -20,14 +20,16 @@ describe('Controller: MembershipController', function () {
module('service.groups'), module('service.groups'),
module('service.profile'), module('service.profile'),
module('service.utility'), module('service.utility'),
module('service.paging'),
module('controller.membership') module('controller.membership')
}); });
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService) { beforeEach(inject(function ($rootScope, $controller, $q, groupsService, profileService, utilityService, pagingService) {
this.rootScope = $rootScope; this.rootScope = $rootScope;
this.scope = $rootScope.$new(); this.scope = $rootScope.$new();
this.groupsService = groupsService; this.groupsService = groupsService;
this.profileService = profileService; this.profileService = profileService;
this.utilityService = utilityService; this.utilityService = utilityService;
this.pagingService = pagingService;
this.q = $q; this.q = $q;
var mockGroup = { var mockGroup = {
data: { data: {
@ -68,6 +70,47 @@ describe('Controller: MembershipController', function () {
this.groupsService.getGroupMemberList = function() { this.groupsService.getGroupMemberList = function() {
return $q.when(mockGroupList); return $q.when(mockGroupList);
}; };
this.groupsService.getGroupChanges = function () {
return $q.when({
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
});
};
this.controller = $controller('MembershipController', {'$scope': this.scope}); this.controller = $controller('MembershipController', {'$scope': this.scope});
this.mockSuccessAlert = "success"; this.mockSuccessAlert = "success";
@ -401,4 +444,169 @@ describe('Controller: MembershipController', function () {
expect(this.scope.membership.members).toEqual(expectedMembership); expect(this.scope.membership.members).toEqual(expectedMembership);
expect(this.scope.isGroupAdmin).toBe(true); expect(this.scope.isGroupAdmin).toBe(true);
}); });
it('test that we properly get group change data', function(){
this.scope.groupChanges = {};
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
this.scope.refresh();
this.scope.$digest();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(this.scope.groupChanges).toEqual(response.data.changes);
});
it('nextPage should call getGroupChanges with the correct parameters', function () {
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedId = "";
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
this.scope.changeNextPage();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
});
it('prevPage should call getGroupChanges with the correct parameters', function () {
var response = {
data: {
changes: [
{
newGroup: {
id: "f9329f39-595d-45c9-8cdf-ac36e96e085d",
name: "test-group",
email: "test@test.com",
created: "2022-07-20T10:14:49Z",
status: "Active",
members: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
],
admins: [
{
id: "ea7ec24e-3cc2-4740-b1b8-acde0158271f"
},
{
id: "5bda099e-be26-4aac-a310-ecf221ee2451"
}
]
},
changeType: "Delete",
userId: "ea7ec24e-3cc2-4740-b1b8-acde0158271f",
id: "13516a79-1c61-4b9d-b442-0a773fc9c99f",
created: "2022-07-20T10:24:28Z",
userName: "professor"
}
],
maxItems: 100
}
};
var getGroupChangesSets = spyOn(this.groupsService, 'getGroupChanges')
.and.stub()
.and.returnValue(this.q.when(response));
var expectedId = "";
var expectedMaxItems = 100;
var expectedStartFrom = undefined;
this.scope.changePrevPage();
expect(getGroupChangesSets.calls.count()).toBe(1);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
this.scope.changeNextPage();
this.scope.changePrevPage();
expect(getGroupChangesSets.calls.count()).toBe(3);
expect(getGroupChangesSets.calls.mostRecent().args).toEqual(
[expectedId, expectedMaxItems, expectedStartFrom]);
});
}); });

View File

@ -53,7 +53,7 @@ angular.module('controller.zones', [])
$scope.currentZone.transferConnection = {}; $scope.currentZone.transferConnection = {};
}; };
groupsService.getGroupsAbridged(true, "").then(function (results) { groupsService.getGroups(true, "").then(function (results) {
if (results.data) { if (results.data) {
// Get all groups where the group members include the current user // Get all groups where the group members include the current user
$scope.myGroups = results.data.groups.filter(grp => grp.members.findIndex(mem => mem.id === $scope.profile.id) >= 0); $scope.myGroups = results.data.groups.filter(grp => grp.members.findIndex(mem => mem.id === $scope.profile.id) >= 0);

View File

@ -39,7 +39,7 @@ describe('Controller: ZonesController', function () {
profileService.getAuthenticatedUserData = function() { profileService.getAuthenticatedUserData = function() {
return $q.when({data: {id: "userId"}}); return $q.when({data: {id: "userId"}});
}; };
groupsService.getGroupsAbridged = function () { groupsService.getGroups = function () {
return $q.when({ return $q.when({
data: { data: {
groups: [{id: "all my groups", members: [{id: "userId"}]}] groups: [{id: "all my groups", members: [{id: "userId"}]}]

View File

@ -56,7 +56,7 @@ angular.module('service.groups', [])
this.getGroupMemberList = function (uuid) { this.getGroupMemberList = function (uuid) {
var url = '/api/groups/' + uuid + '/members'; var url = '/api/groups/' + uuid + '/members';
url = this.urlBuilder(url, { maxItems: 1000 }); url = this.urlBuilder(url, { maxItems: 100 });
return $http.get(url); return $http.get(url);
}; };
@ -75,7 +75,7 @@ angular.module('service.groups', [])
query = null; query = null;
} }
var params = { var params = {
"maxItems": 1500, "maxItems": 100,
"groupNameFilter": query, "groupNameFilter": query,
"ignoreAccess": ignoreAccess "ignoreAccess": ignoreAccess
}; };
@ -84,12 +84,13 @@ angular.module('service.groups', [])
return $http.get(url); return $http.get(url);
}; };
this.getGroupsAbridged = function (ignoreAccess, query) { this.getGroupsAbridged = function (limit, startFrom, ignoreAccess, query) {
if (query == "") { if (query == "") {
query = null; query = null;
} }
var params = { var params = {
"maxItems": 1500, "maxItems": limit,
"startFrom": startFrom,
"groupNameFilter": query, "groupNameFilter": query,
"ignoreAccess": ignoreAccess, "ignoreAccess": ignoreAccess,
"abridged": true "abridged": true
@ -105,6 +106,12 @@ angular.module('service.groups', [])
return $http.get(url); return $http.get(url);
}; };
this.getGroupChanges = function (groupId, count, startFrom) {
var url = '/api/groups/' + groupId + '/groupchanges';
url = this.urlBuilder(url, { 'startFrom': startFrom, 'maxItems': count });
return $http.get(url);
};
this.getGroupsStored = function () { this.getGroupsStored = function () {
if (_refreshMyGroups || _myGroupsPromise == undefined) { if (_refreshMyGroups || _myGroupsPromise == undefined) {
_myGroupsPromise = this.getGroups().then( _myGroupsPromise = this.getGroups().then(

View File

@ -26,6 +26,10 @@ angular.module('service.profile', [])
return $http.get('/api/users/lookupuser/' + username); return $http.get('/api/users/lookupuser/' + username);
} }
this.getUserDataById = function(userId){
return $http.get('/api/users/' + userId);
}
this.regenerateCredentials = function(){ this.regenerateCredentials = function(){
return $http.post('/regenerate-creds', {}, {headers: utilityService.getCsrfHeader()}); return $http.post('/regenerate-creds', {}, {headers: utilityService.getCsrfHeader()});
} }

View File

@ -43,6 +43,10 @@ describe('Service: profileService', function () {
expect(this.profileService.getUserDataByUsername).toBeDefined(); expect(this.profileService.getUserDataByUsername).toBeDefined();
}); });
it('should have getUserDataById method', function () {
expect(this.profileService.getUserDataByUsername).toBeDefined();
});
it('should have regenerateCredentials method', function () { it('should have regenerateCredentials method', function () {
expect(this.profileService.regenerateCredentials()).toBeDefined(); expect(this.profileService.regenerateCredentials()).toBeDefined();
}); });
@ -119,6 +123,39 @@ describe('Service: profileService', function () {
this.$httpBackend.flush(); this.$httpBackend.flush();
}); });
it('getUserDataByUserId method should return 200 with valid user', function (done) {
this.$httpBackend.expectGET('/api/users/userId').respond('success');
this.profileService.getUserDataById('userId')
.then(function (response) {
expect(response.status).toBe(200);
expect(response.data).toBe('success');
done();
}, function (error) {
fail('lookupUserAccount expected 200, but got ' + error.status.toString());
done();
});
this.$httpBackend.flush();
});
it('getUserDataByUserId method should return 400 with invalid user', function (done) {
var url = '/api/users/:userId';
this.$httpBackend.whenRoute('GET', url)
.respond(function () {
return [400, 'response body', {}, 'TestPhrase'];
});
this.profileService.getUserDataById('badUserId')
.then(function (response) {
fail('lookupUserAccount expected 400, but got ' + response.status.toString());
done();
}, function (error) {
expect(error.status).toBe(400);
done();
});
this.$httpBackend.flush();
});
it('regenerateCredentials method should return 400 with invalid user', function (done) { it('regenerateCredentials method should return 400 with invalid user', function (done) {
var url = '/regenerate-creds'; var url = '/regenerate-creds';
this.$httpBackend.whenRoute('POST', url).respond(400); this.$httpBackend.whenRoute('POST', url).respond(400);

View File

@ -43,6 +43,15 @@ angular.module('service.zones', [])
return promis return promis
}; };
this.getZoneChanges = function (limit, startFrom, zoneId) {
var params = {
"maxItems": limit,
"startFrom": startFrom
}
var url = utilityService.urlBuilder ( "/api/zones/" + zoneId + "/changes", params);
return $http.get(url);
};
this.getBackendIds = function() { this.getBackendIds = function() {
var url = "/api/zones/backendids"; var url = "/api/zones/backendids";
return $http.get(url); return $http.get(url);

View File

@ -35,6 +35,15 @@ describe('Service: zoneService', function () {
this.$httpBackend.flush(); this.$httpBackend.flush();
}); });
it('http backend gets called properly when getting zoneChanges', function () {
this.$httpBackend.expectGET('/api/zones/zoneid/changes?maxItems=100&startFrom=start').respond('zoneChanges returned');
this.zonesService.getZoneChanges('100', 'start', 'zoneid', false)
.then(function(response) {
expect(response.data).toBe('zoneChanges returned');
});
this.$httpBackend.flush();
});
it('http backend gets called properly when sending zone', function (done) { it('http backend gets called properly when sending zone', function (done) {
this.$httpBackend.expectPOST('/api/zones').respond('zone sent'); this.$httpBackend.expectPOST('/api/zones').respond('zone sent');
this.zonesService.sendZone('zone payload') this.zonesService.sendZone('zone payload')

View File

@ -145,6 +145,48 @@ trait TestApplicationData { this: Mockito =>
| } | }
""".stripMargin) """.stripMargin)
val hobbitGroupChangeId = "b6018a9b-c893-40e9-aa25-4ccfee460c18"
val hobbitGroupChange: JsValue = Json.parse(s"""{
| "newGroup": {
| "id": "$hobbitGroupId",
| "name": "hobbits",
| "email": "hobbitAdmin@shire.me",
| "description": "Hobbits of the shire",
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
| "admins": [ { "id": "${frodoUser.id}" } ]
| },
| "changeType": "Create",
| "userId": "${frodoUser.id}",
| "oldGroup": {},
| "id": "b6018a9b-c893-40e9-aa25-4ccfee460c18",
| "created": "2022-07-22T08:19:22Z",
| "userName": "$frodoUser",
| "groupChangeMessage": ""
| }
""".stripMargin)
val hobbitGroupChanges: JsValue = Json.parse(s"""{
| "changes": [{
| "newGroup": {
| "id": "$hobbitGroupId",
| "name": "hobbits",
| "email": "hobbitAdmin@shire.me",
| "description": "Hobbits of the shire",
| "members": [ { "id": "${frodoUser.id}" }, { "id": "samwise-userId" } ],
| "admins": [ { "id": "${frodoUser.id}" } ]
| },
| "changeType": "Create",
| "userId": "${frodoUser.id}",
| "oldGroup": {},
| "id": "b6018a9b-c893-40e9-aa25-4ccfee460c18",
| "created": "2022-07-22T08:19:22Z",
| "userName": "$frodoUser",
| "groupChangeMessage": ""
| }],
| "maxItems": 100
| }
""".stripMargin)
val ringbearerGroup: JsValue = Json.parse( val ringbearerGroup: JsValue = Json.parse(
s"""{ s"""{
| "id": "ringbearer-group-uuid", | "id": "ringbearer-group-uuid",
@ -196,6 +238,25 @@ trait TestApplicationData { this: Mockito =>
| } | }
""".stripMargin) """.stripMargin)
val hobbitZoneChange: JsValue = Json.parse(s"""{
| "zoneId": "$hobbitZoneId",
| "zoneChanges":
| [{ "zone": {
| "name": "$hobbitZoneName",
| "email": "hobbitAdmin@shire.me",
| "status": "Active",
| "account": "system",
| "acl": "rules",
| "adminGroupId": "$hobbitGroupId",
| "id": "$hobbitZoneId",
| "shared": false,
| "status": "Active",
| "isTest": true
| }}],
| "maxItems": 100
| }
""".stripMargin)
val hobbitZoneRequest: JsValue = Json.parse(s"""{ val hobbitZoneRequest: JsValue = Json.parse(s"""{
| "name": "hobbits", | "name": "hobbits",
| "email": "hobbitAdmin@shire.me", | "email": "hobbitAdmin@shire.me",

View File

@ -800,6 +800,154 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
} }
} }
".getGroupChange" should {
tag("slow")
"return the group change if it is found - status ok (200)" in new WithApplication(app) {
val client = MockWS {
case (GET, u) if u == s"http://localhost:9001/groups/change/${hobbitGroupChangeId}" =>
defaultActionBuilder { Results.Ok(hobbitGroupChange) }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result =
underTest.getGroupChange(hobbitGroupChangeId)(
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(OK)
hasCacheHeaders(result)
contentAsJson(result) must beEqualTo(hobbitGroupChange)
}
"return authentication failed (401) when auth fails in the backend" in new WithApplication(
app
) {
val client = MockWS {
case (GET, u) if u == s"http://localhost:9001/groups/change/${hobbitGroupChangeId}" =>
defaultActionBuilder { Results.Unauthorized("Invalid credentials") }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result =
underTest.getGroupChange(hobbitGroupChangeId)(
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(UNAUTHORIZED)
hasCacheHeaders(result)
}
"return a not found (404) if the group change does not exist" in new WithApplication(app) {
val client = MockWS {
case (GET, u) if u == "http://localhost:9001/groups/change/not-hobbits" =>
defaultActionBuilder { Results.NotFound }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result = underTest.getGroupChange("not-hobbits")(
FakeRequest(GET, "/groups/change/not-hobbits")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(NOT_FOUND)
hasCacheHeaders(result)
}
"return status forbidden (403) if the user account is locked" in new WithApplication(app) {
val client = mock[WSClient]
val underTest =
TestVinylDNS(
testConfigLdap,
mockLdapAuthenticator,
mockLockedUserAccessor,
client,
components,
crypto,
mockOidcAuth
)
val result =
underTest.getGroupChange(hobbitGroupChangeId)(
FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId")
.withSession(
"username" -> lockedFrodoUser.userName,
"accessKey" -> lockedFrodoUser.accessKey
)
)
status(result) mustEqual 403
hasCacheHeaders(result)
contentAsString(result) must beEqualTo(
s"User account for `${lockedFrodoUser.userName}` is locked."
)
}
"return unauthorized (401) if user is not logged in" in new WithApplication(app) {
val client = mock[WSClient]
val underTest = withClient(client)
val result =
underTest.getGroupChange(hobbitGroupChangeId)(FakeRequest(GET, s"/groups/change/$hobbitGroupChangeId"))
status(result) must beEqualTo(401)
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
hasCacheHeaders(result)
}
}
".listGroupChanges" should {
"return group changes - status ok (200)" in new WithApplication(app) {
val client = MockWS {
case (GET, u) if u == s"http://localhost:9001/groups/${hobbitGroupId}/activity" =>
defaultActionBuilder { Results.Ok(hobbitGroupChanges) }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result =
underTest.listGroupChanges(hobbitGroupId)(
FakeRequest(GET, s"/groups/$hobbitGroupId/activity")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(OK)
hasCacheHeaders(result)
contentAsJson(result) must beEqualTo(hobbitGroupChanges)
}
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
val client = mock[WSClient]
val underTest = withClient(client)
val result =
underTest.listGroupChanges(hobbitGroupId)(
FakeRequest(GET, s"/api/groups/$hobbitGroupId/activity")
)
status(result) mustEqual 401
hasCacheHeaders(result)
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
}
"return forbidden (403) if user account is locked" in new WithApplication(app) {
val client = mock[WSClient]
val underTest = withLockedClient(client)
val result = underTest.listGroupChanges(hobbitGroupId)(
FakeRequest(GET, s"/api/groups/$hobbitGroupId/activity").withSession(
"username" -> lockedFrodoUser.userName,
"accessKey" -> lockedFrodoUser.accessKey
)
)
status(result) mustEqual 403
hasCacheHeaders(result)
contentAsString(result) must beEqualTo(
s"User account for `${lockedFrodoUser.userName}` is locked."
)
}
}
".deleteGroup" should { ".deleteGroup" should {
"return ok with no content (204) when delete is successful" in new WithApplication(app) { "return ok with no content (204) when delete is successful" in new WithApplication(app) {
val client = MockWS { val client = MockWS {
@ -1502,6 +1650,78 @@ class VinylDNSSpec extends Specification with Mockito with TestApplicationData w
} }
} }
".getZoneChange" should {
"return ok (200) if the zoneChanges is found" in new WithApplication(app) {
val client = MockWS {
case (GET, u) if u == s"http://localhost:9001/zones/$hobbitZoneId/changes" =>
defaultActionBuilder { Results.Ok(hobbitZoneChange) }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result =
underTest.getZoneChange(hobbitZoneId)(
FakeRequest(GET, s"/zones/$hobbitZoneId/changes")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(OK)
hasCacheHeaders(result)
contentAsJson(result) must beEqualTo(hobbitZoneChange)
}
"return a not found (404) if the zoneChanges does not exist" in new WithApplication(app) {
val client = MockWS {
case (GET, u) if u == s"http://localhost:9001/zones/not-hobbits/changes" =>
defaultActionBuilder { Results.NotFound }
}
val mockUserAccessor = mock[UserAccountAccessor]
mockUserAccessor.get(anyString).returns(IO.pure(Some(frodoUser)))
mockUserAccessor.getUserByKey(anyString).returns(IO.pure(Some(frodoUser)))
val underTest = withClient(client)
val result =
underTest.getZoneChange("not-hobbits")(
FakeRequest(GET, "/zones/not-hobbits/changes")
.withSession("username" -> frodoUser.userName, "accessKey" -> frodoUser.accessKey)
)
status(result) must beEqualTo(NOT_FOUND)
hasCacheHeaders(result)
}
"return unauthorized (401) if requesting user is not logged in" in new WithApplication(app) {
val client = mock[WSClient]
val underTest = withClient(client)
val result =
underTest.getZoneChange(hobbitZoneId)(
FakeRequest(GET, s"/api/zones/$hobbitZoneId/changes")
)
status(result) mustEqual 401
hasCacheHeaders(result)
contentAsString(result) must beEqualTo("You are not logged in. Please login to continue.")
}
"return forbidden (403) if user account is locked" in new WithApplication(app) {
val client = mock[WSClient]
val underTest = withLockedClient(client)
val result = underTest.getZoneChange(hobbitZoneId)(
FakeRequest(GET, s"/api/zones/$hobbitZoneId/changes").withSession(
"username" -> lockedFrodoUser.userName,
"accessKey" -> lockedFrodoUser.accessKey
)
)
status(result) mustEqual 403
hasCacheHeaders(result)
contentAsString(result) must beEqualTo(
s"User account for `${lockedFrodoUser.userName}` is locked."
)
}
}
".getZoneByName" should { ".getZoneByName" should {
"return ok (200) if the zone is found" in new WithApplication(app) { "return ok (200) if the zone is found" in new WithApplication(app) {
val client = MockWS { val client = MockWS {