mirror of
https://github.com/VinylDNS/vinyldns
synced 2025-08-22 02:02:14 +00:00
Merge branch 'master' into JoshSEdwards/documentation-for-apple-m1-support
This commit is contained in:
commit
41d9c91a3f
@ -110,8 +110,8 @@ vinyldns {
|
||||
limits {
|
||||
batchchange-routing-max-items-limit = 100
|
||||
membership-routing-default-max-items = 100
|
||||
membership-routing-max-items-limit = 1000
|
||||
membership-routing-max-groups-list-limit = 1500
|
||||
membership-routing-max-items-limit = 100
|
||||
membership-routing-max-groups-list-limit = 100
|
||||
recordset-routing-default-max-items= 100
|
||||
zone-routing-default-max-items = 100
|
||||
zone-routing-max-items-limit = 100
|
||||
|
@ -27,12 +27,15 @@ import vinyldns.api.domain.record.RecordSetChangeGenerator
|
||||
import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
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
|
||||
|
||||
class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue: MessageQueue)
|
||||
extends BatchChangeConverterAlgebra {
|
||||
|
||||
private val notExistCompletedMessage: String = "This record does not exist." +
|
||||
"No further action is required."
|
||||
private val failedMessage: String = "Error queueing RecordSetChange for processing"
|
||||
private val logger = LoggerFactory.getLogger(classOf[BatchChangeConverter])
|
||||
|
||||
def sendBatchForProcessing(
|
||||
@ -68,6 +71,12 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
): BatchResult[Unit] = {
|
||||
val convertedIds = recordSetChanges.flatMap(_.singleBatchChangeIds).toSet
|
||||
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 None =>
|
||||
logger.info(s"Successfully converted SingleChanges [${singleChanges
|
||||
@ -104,21 +113,33 @@ class BatchChangeConverter(batchChangeRepo: BatchChangeRepository, messageQueue:
|
||||
rsChange.singleBatchChangeIds.map(batchId => (batchId, rsChange.id))
|
||||
}.toMap
|
||||
val withStatus = batchChange.changes.map { change =>
|
||||
idsMap
|
||||
.get(change.id)
|
||||
.map { _ => change } // a recordsetchange was successfully queued for this change
|
||||
.getOrElse {
|
||||
// failure here means there was a message queue issue for this change
|
||||
change.withFailureMessage("Error queueing RecordSetChange for processing")
|
||||
idsMap
|
||||
.get(change.id)
|
||||
.map { _ =>
|
||||
// a recordsetchange was successfully queued for this change
|
||||
change
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
||||
def storeQueuingFailures(batchChange: BatchChange): BatchResult[Unit] = {
|
||||
val failedChanges = batchChange.changes.collect {
|
||||
case change if change.status == SingleChangeStatus.Failed => change }
|
||||
batchChangeRepo.updateSingleChanges(failedChanges).as(())
|
||||
// Update if Single change is Failed or if a record that does not exist is deleted
|
||||
val failedAndNotExistsChanges = batchChange.changes.collect {
|
||||
case change if change.status == SingleChangeStatus.Failed || change.systemMessage.contains(notExistCompletedMessage) => change
|
||||
}
|
||||
batchChangeRepo.updateSingleChanges(failedAndNotExistsChanges).as(())
|
||||
}.toBatchResult
|
||||
|
||||
def createRecordSetChangesForBatch(
|
||||
|
@ -348,7 +348,7 @@ class BatchChangeValidations(
|
||||
userCanDeleteRecordSet(change, auth, rs.ownerGroupId, rs.records) |+|
|
||||
zoneDoesNotRequireManualReview(change, isApproved) |+|
|
||||
ensureRecordExists(change, groupedChanges)
|
||||
case None => RecordDoesNotExist(change.inputChange.inputName).invalidNel
|
||||
case None => RecordDoesNotExist(change.inputChange.inputName).validNel
|
||||
}
|
||||
validations.map(_ => change)
|
||||
}
|
||||
@ -402,7 +402,7 @@ class BatchChangeValidations(
|
||||
zoneDoesNotRequireManualReview(change, isApproved) |+|
|
||||
ensureRecordExists(change, groupedChanges)
|
||||
case None =>
|
||||
RecordDoesNotExist(change.inputChange.inputName).invalidNel
|
||||
RecordDoesNotExist(change.inputChange.inputName).validNel
|
||||
}
|
||||
|
||||
validations.map(_ => change)
|
||||
|
@ -59,7 +59,9 @@ final case class GroupChangeInfo(
|
||||
userId: String,
|
||||
oldGroup: Option[GroupInfo] = None,
|
||||
id: String = UUID.randomUUID().toString,
|
||||
created: String = DateTime.now.getMillis.toString
|
||||
created: DateTime = DateTime.now,
|
||||
userName: String,
|
||||
groupChangeMessage: String
|
||||
)
|
||||
|
||||
object GroupChangeInfo {
|
||||
@ -69,7 +71,9 @@ object GroupChangeInfo {
|
||||
userId = groupChange.userId,
|
||||
oldGroup = groupChange.oldGroup.map(GroupInfo.apply),
|
||||
id = groupChange.id,
|
||||
created = groupChange.created.getMillis.toString
|
||||
created = groupChange.created,
|
||||
userName = groupChange.userName.getOrElse("unknown user"),
|
||||
groupChangeMessage = groupChange.groupChangeMessage.getOrElse("")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -216,12 +216,18 @@ class MembershipService(
|
||||
): ListMyGroupsResponse = {
|
||||
val allMyGroups = allGroups
|
||||
.filter(_.status == GroupStatus.Active)
|
||||
.sortBy(_.id)
|
||||
.sortBy(_.name.toLowerCase)
|
||||
.map(x => GroupInfo.fromGroup(x, abridged, Some(authPrincipal)))
|
||||
|
||||
val filtered = allMyGroups
|
||||
.filter(grp => groupNameFilter.map(_.toLowerCase).forall(grp.name.toLowerCase.contains(_)))
|
||||
.filter(grp => startFrom.forall(grp.id > _))
|
||||
val filtered = if(startFrom.isDefined){
|
||||
val prevPageGroup = allMyGroups.filter(_.id == startFrom.get).head.name
|
||||
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 groups = filtered.take(maxItems)
|
||||
@ -229,6 +235,23 @@ class MembershipService(
|
||||
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(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
@ -240,13 +263,65 @@ class MembershipService(
|
||||
result <- groupChangeRepo
|
||||
.getGroupChanges(groupId, startFrom, maxItems)
|
||||
.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(
|
||||
result.changes.map(GroupChangeInfo.apply),
|
||||
groupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))),
|
||||
startFrom,
|
||||
result.lastEvaluatedTimeStamp,
|
||||
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
|
||||
* @param userIdentifier The userId or username
|
||||
|
@ -39,6 +39,8 @@ trait MembershipServiceAlgebra {
|
||||
|
||||
def getGroup(id: String, authPrincipal: AuthPrincipal): Result[Group]
|
||||
|
||||
def getGroupChange(id: String, authPrincipal: AuthPrincipal): Result[GroupChangeInfo]
|
||||
|
||||
def listMyGroups(
|
||||
groupNameFilter: Option[String],
|
||||
startFrom: Option[String],
|
||||
|
@ -19,7 +19,7 @@ package vinyldns.api.domain.membership
|
||||
import vinyldns.api.Interfaces.ensuring
|
||||
import vinyldns.core.domain.auth.AuthPrincipal
|
||||
import vinyldns.api.domain.zone.NotAuthorizedError
|
||||
import vinyldns.core.domain.membership.Group
|
||||
import vinyldns.core.domain.membership.{Group, GroupChange}
|
||||
|
||||
object MembershipValidations {
|
||||
|
||||
@ -44,4 +44,9 @@ object MembershipValidations {
|
||||
ensuring(NotAuthorizedError("Not authorized")) {
|
||||
authPrincipal.isGroupMember(groupId) || authPrincipal.isSystemAdmin || canViewGroupDetails
|
||||
}
|
||||
|
||||
def isGroupChangePresent(groupChange: Option[GroupChange]): Either[Throwable, Unit] =
|
||||
ensuring(InvalidGroupRequestError("Invalid Group Change ID")) {
|
||||
groupChange.isDefined
|
||||
}
|
||||
}
|
||||
|
@ -120,7 +120,9 @@ trait MembershipJsonProtocol extends JsonValidation {
|
||||
(js \ "userId").required[String]("Missing userId"),
|
||||
(js \ "oldGroup").optional[GroupInfo],
|
||||
(js \ "id").default[String](UUID.randomUUID().toString),
|
||||
(js \ "created").default[String](DateTime.now.getMillis.toString)
|
||||
).mapN(GroupChangeInfo.apply)
|
||||
(js \ "created").default[DateTime](DateTime.now),
|
||||
(js \ "userName").required[String]("Missing userName"),
|
||||
(js \ "groupChangeMessage").required[String]("Missing groupChangeMessage"),
|
||||
).mapN(GroupChangeInfo.apply)
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class MembershipRoute(
|
||||
} ~
|
||||
(get & monitor("Endpoint.listMyGroups")) {
|
||||
parameters(
|
||||
"startFrom".?,
|
||||
"startFrom".as[String].?,
|
||||
"maxItems".as[Int].?(DEFAULT_MAX_ITEMS),
|
||||
"groupNameFilter".?,
|
||||
"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 =>
|
||||
(put & monitor("Endpoint.lockUser")) {
|
||||
authenticateAndExecute(membershipService.updateUserLockStatus(id, LockStatus.Locked, _)) {
|
||||
|
@ -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_update_fqdn, change_type="DeleteRecordSet"),
|
||||
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
|
||||
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"),
|
||||
|
||||
# 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_update_dummy_fqdn, change_type="DeleteRecordSet"),
|
||||
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[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[3], input_name=f"non-existent.{ok_zone_name}", change_type="DeleteRecordSet")
|
||||
|
||||
# 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",
|
||||
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.'])
|
||||
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",
|
||||
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.'])
|
||||
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, '
|
||||
'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",
|
||||
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.'])
|
||||
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." '
|
||||
'and type "A" is not allowed in a reverse zone.',
|
||||
'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",
|
||||
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.'])
|
||||
|
||||
# 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",
|
||||
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.'])
|
||||
|
||||
# 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,
|
||||
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.'])
|
||||
@ -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_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(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
|
||||
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"),
|
||||
|
||||
# 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(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"),
|
||||
@ -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")
|
||||
assert_successful_change_in_error_response(response[2], input_name=rs_update_fqdn, record_type="AAAA",
|
||||
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
|
||||
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",
|
||||
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.'])
|
||||
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",
|
||||
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."])
|
||||
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",
|
||||
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.'])
|
||||
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",
|
||||
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'valid domain names must be letters, numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
|
||||
|
||||
# zone discovery failure
|
||||
assert_failed_change_in_error_response(response[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",
|
||||
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
|
||||
"If zone exists, then it must be connected to in VinylDNS."])
|
||||
|
||||
# context validation failures: record does not exist, not authorized
|
||||
assert_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_failed_change_in_error_response(response[11], input_name=rs_delete_dummy_fqdn,
|
||||
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"update3.{ok_zone_name}", change_type="DeleteRecordSet"),
|
||||
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
|
||||
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"),
|
||||
|
||||
# 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"delete-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")
|
||||
assert_successful_change_in_error_response(response[2], input_name=f"update3.{ok_zone_name}", record_type="CNAME", ttl=300,
|
||||
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
|
||||
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")
|
||||
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")
|
||||
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.")
|
||||
|
||||
# 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",
|
||||
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.'])
|
||||
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",
|
||||
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.'])
|
||||
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.",
|
||||
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, '
|
||||
@ -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.'])
|
||||
|
||||
# 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",
|
||||
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.'])
|
||||
|
||||
# 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}",
|
||||
record_type="CNAME", record_data="test.com.")
|
||||
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}.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}.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
|
||||
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"),
|
||||
|
||||
# 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", change_type="DeleteRecordSet"),
|
||||
]
|
||||
}
|
||||
|
||||
@ -2422,25 +2413,29 @@ def test_ipv4_ptr_recordtype_update_delete_checks(shared_zone_test_context):
|
||||
record_data="has-updated.ptr.")
|
||||
assert_successful_change_in_error_response(response[2], input_name=f"{ip4_prefix}.193", record_type="PTR",
|
||||
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
|
||||
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")
|
||||
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.")
|
||||
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.")
|
||||
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")
|
||||
|
||||
# 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",
|
||||
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",
|
||||
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.",
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
|
||||
'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.'])
|
||||
|
||||
# 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",
|
||||
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."])
|
||||
|
||||
# context validation failures: record does not exist
|
||||
assert_failed_change_in_error_response(response[11], input_name=f"{ip4_prefix}.199", record_type="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."])
|
||||
assert_successful_change_in_error_response(response[13], ttl=300, input_name=f"{ip4_prefix}.200", record_type="PTR", record_data="has-updated.ptr.")
|
||||
finally:
|
||||
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::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::60", change_type="DeleteRecordSet"),
|
||||
get_change_PTR_json(f"{ip6_prefix}:1000::65", change_type="DeleteRecordSet"),
|
||||
|
||||
# input validations failures
|
||||
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"),
|
||||
|
||||
# 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", 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.")
|
||||
assert_successful_change_in_error_response(response[2], input_name=f"{ip6_prefix}:1000::62", record_type="PTR",
|
||||
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
|
||||
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",
|
||||
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",
|
||||
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.",
|
||||
error_messages=['Invalid TTL: "29", must be a number between 30 and 2147483647.',
|
||||
'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.'])
|
||||
|
||||
# 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",
|
||||
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."])
|
||||
|
||||
# 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",
|
||||
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",
|
||||
assert_successful_change_in_error_response(response[9], ttl=300, input_name=f"{ip6_prefix}:1000::65",
|
||||
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:
|
||||
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_update_fqdn, change_type="DeleteRecordSet"),
|
||||
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
|
||||
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"),
|
||||
|
||||
# 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(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
|
||||
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[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[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
|
||||
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 '
|
||||
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.'])
|
||||
|
||||
# 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=[
|
||||
"Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
|
||||
"If zone exists, then it must be connected to in VinylDNS."])
|
||||
|
||||
# context validation failures: record does not exist, not authorized
|
||||
assert_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_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."])
|
||||
@ -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_update_fqdn, change_type="DeleteRecordSet"),
|
||||
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
|
||||
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"),
|
||||
|
||||
# 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(rs_delete_dummy_fqdn, change_type="DeleteRecordSet"),
|
||||
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[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[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
|
||||
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",
|
||||
error_messages=[f'Invalid domain name: "invalid-name$.{ok_zone_name}", valid domain names must be letters, '
|
||||
f'numbers, underscores, and hyphens, joined by dots, and terminated with a dot.'])
|
||||
assert_failed_change_in_error_response(response[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."},
|
||||
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."},
|
||||
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.'])
|
||||
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."},
|
||||
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.'])
|
||||
|
||||
# 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",
|
||||
error_messages=["Zone Discovery Failed: zone for \"no.zone.at.all.\" does not exist in VinylDNS. "
|
||||
"If zone exists, then it must be connected to in VinylDNS."])
|
||||
|
||||
# context validation failures: record does not exist, not authorized
|
||||
assert_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",
|
||||
record_data={"preference": 1000, "exchange": "foo.bar."})
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
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 = {
|
||||
"comments": "test delete record failures",
|
||||
"changes": [
|
||||
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")
|
||||
get_change_A_AAAA_json(f"delete-non-existent-record.{ok_zone_name}", change_type="DeleteRecordSet")
|
||||
]
|
||||
}
|
||||
|
||||
to_delete = []
|
||||
response = client.create_batch_change(batch_change_input, status=202)
|
||||
get_batch = client.get_batch_change(response["id"])
|
||||
|
||||
try:
|
||||
create_rs = client.create_recordset(a_delete, status=202)
|
||||
to_delete.append(client.wait_until_recordset_change_status(create_rs, "Complete"))
|
||||
assert_that(get_batch["changes"][0]["systemMessage"], is_("This record does not exist." +
|
||||
"No further action is required."))
|
||||
|
||||
errors = client.create_batch_change(batch_change_input, status=400)
|
||||
|
||||
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)
|
||||
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")
|
||||
|
||||
|
||||
@pytest.mark.serial
|
||||
|
@ -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
|
||||
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
|
||||
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["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()))
|
||||
|
||||
# we should have, in order, changes 8 7 6 5 4
|
||||
|
@ -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
|
||||
"""
|
||||
results = list_my_groups_context.client.list_my_groups(status=200)
|
||||
assert_that(results, has_length(3)) # 3 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, has_length(4)) # 4 fields
|
||||
assert_that(results, is_not(has_key("groupNameFilter")))
|
||||
assert_that(results, is_not(has_key("startFrom")))
|
||||
assert_that(results, is_not(has_key("nextId")))
|
||||
assert_that(results["maxItems"], is_(200))
|
||||
|
||||
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)))
|
||||
assert_that(results, is_(has_key("nextId")))
|
||||
assert_that(results["maxItems"], is_(100))
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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("startFrom")))
|
||||
assert_that(results, is_not(has_key("nextId")))
|
||||
assert_that(results["maxItems"], is_(200))
|
||||
assert_that(results, is_(has_key("nextId")))
|
||||
assert_that(results["maxItems"], is_(100))
|
||||
|
||||
|
||||
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, is_not(has_key("startFrom")))
|
||||
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"])
|
||||
|
||||
@ -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
|
||||
"""
|
||||
results = list_my_groups_context.client.list_my_groups(ignore_access=True, status=200)
|
||||
|
||||
# Only count the groups with the group prefix
|
||||
assert_that(results, has_length(4)) # 4 fields
|
||||
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))
|
||||
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
|
||||
assert_that(results, has_length(4)) # 4 fields
|
||||
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))
|
||||
|
||||
|
||||
@ -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
|
||||
"""
|
||||
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(results["maxItems"], is_(200))
|
||||
assert_that(results["maxItems"], is_(100))
|
||||
assert_that(results["ignoreAccess"], is_(True))
|
||||
|
@ -220,7 +220,7 @@ class VinylDNSClient(object):
|
||||
|
||||
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
|
||||
:param start_from: the start key of the page
|
||||
|
@ -37,6 +37,8 @@ import vinyldns.core.domain.record._
|
||||
import vinyldns.core.domain.zone.Zone
|
||||
|
||||
class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelpers {
|
||||
private val notExistCompletedMessage: String = "This record does not exist." +
|
||||
"No further action is required."
|
||||
|
||||
private def makeSingleAddChange(
|
||||
name: String,
|
||||
@ -160,6 +162,14 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
|
||||
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(
|
||||
makeSingleAddChange("one", AData("1.1.1.1")),
|
||||
makeSingleAddChange("two", AData("1.1.1.2")),
|
||||
@ -535,6 +545,42 @@ class BatchChangeConverterSpec extends AnyWordSpec with Matchers with CatsHelper
|
||||
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 {
|
||||
val batchChangeUnsupported =
|
||||
BatchChange(
|
||||
|
@ -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 deleteRecord = makeDeleteUpdateDeleteRRSet("deleteRecord", 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(1) should haveInvalid[DomainValidationError](
|
||||
RecordDoesNotExist(deleteRRSet.inputChange.inputName)
|
||||
)
|
||||
result(3) should haveInvalid[DomainValidationError](
|
||||
RecordDoesNotExist(deleteRecord.inputChange.inputName)
|
||||
)
|
||||
result(3) should haveInvalid[DomainValidationError](
|
||||
RecordDoesNotExist(deleteRecord.inputChange.inputName)
|
||||
)
|
||||
result(1) shouldBe valid
|
||||
result(3) shouldBe valid
|
||||
result(4) shouldBe valid
|
||||
deleteNonExistentEntry.inputChange.record.foreach { record =>
|
||||
result(5) should haveInvalid[DomainValidationError](
|
||||
@ -1589,7 +1582,7 @@ class BatchChangeValidationsSpec
|
||||
}
|
||||
|
||||
property(
|
||||
"""validateChangesWithContext: should fail DeleteChangeForValidation with RecordDoesNotExist
|
||||
"""validateChangesWithContext: should complete DeleteChangeForValidation
|
||||
|if record does not exist""".stripMargin
|
||||
) {
|
||||
val deleteRRSet = makeDeleteUpdateDeleteRRSet("record-does-not-exist")
|
||||
@ -1606,12 +1599,8 @@ class BatchChangeValidationsSpec
|
||||
None
|
||||
)
|
||||
|
||||
result(0) should haveInvalid[DomainValidationError](
|
||||
RecordDoesNotExist(deleteRRSet.inputChange.inputName)
|
||||
)
|
||||
result(1) should haveInvalid[DomainValidationError](
|
||||
RecordDoesNotExist(deleteRecord.inputChange.inputName)
|
||||
)
|
||||
result(0) shouldBe valid
|
||||
result(1) shouldBe valid
|
||||
}
|
||||
|
||||
property("""validateChangesWithContext: should succeed for DeleteChangeForValidation
|
||||
|
@ -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 {
|
||||
"return the group activity" in {
|
||||
val groupChangeRepoResponse = ListGroupChangesResults(
|
||||
@ -793,8 +846,13 @@ class MembershipServiceSpec
|
||||
.when(mockGroupChangeRepo)
|
||||
.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] =
|
||||
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100)
|
||||
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
|
||||
|
||||
val result: ListGroupChangesResponse =
|
||||
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, dummyAuth).value)
|
||||
@ -813,8 +871,13 @@ class MembershipServiceSpec
|
||||
.when(mockGroupChangeRepo)
|
||||
.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] =
|
||||
listOfDummyGroupChanges.map(GroupChangeInfo.apply).take(100)
|
||||
listOfDummyGroupChanges.map(change => GroupChangeInfo.apply(change.copy(userName = userMap.get(change.userId)))).take(100)
|
||||
|
||||
val result: ListGroupChangesResponse =
|
||||
rightResultOf(underTest.getGroupActivity(dummyGroup.id, None, 100, okAuth).value)
|
||||
@ -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 {
|
||||
"return a list of admins" in {
|
||||
val testGroup =
|
||||
|
@ -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]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
"return a 200 response with the user locked" in {
|
||||
membershipRoute = superUserRoute
|
||||
|
@ -46,6 +46,13 @@ sealed trait SingleChange {
|
||||
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 =
|
||||
this match {
|
||||
case add: SingleAddChange =>
|
||||
|
@ -34,7 +34,9 @@ case class GroupChange(
|
||||
userId: String,
|
||||
oldGroup: Option[Group] = None,
|
||||
id: String = UUID.randomUUID().toString,
|
||||
created: DateTime = DateTime.now
|
||||
created: DateTime = DateTime.now,
|
||||
userName: Option[String] = None,
|
||||
groupChangeMessage: Option[String] = None
|
||||
)
|
||||
|
||||
object GroupChange {
|
||||
|
@ -24,7 +24,8 @@ import vinyldns.core.repository.Repository
|
||||
trait GroupChangeRepository extends Repository {
|
||||
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(
|
||||
groupId: String,
|
||||
startFrom: Option[String],
|
||||
|
@ -157,4 +157,13 @@ object TestMembershipData {
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
@ -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 =>
|
||||
val vinyldnsRequest = VinylDNSRequest("GET", s"$vinyldnsServiceBackend", s"users/$id")
|
||||
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 =>
|
||||
// $COVERAGE-OFF$
|
||||
val vinyldnsRequest =
|
||||
|
@ -2,110 +2,281 @@
|
||||
|
||||
@content = {
|
||||
|
||||
<!-- PAGE CONTENT -->
|
||||
<div class="right_col" role="main" >
|
||||
<!-- PAGE CONTENT -->
|
||||
<div class="right_col" role="main" >
|
||||
|
||||
<div>
|
||||
<!-- START BREADCRUMB -->
|
||||
<ul class="breadcrumb">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/groups">Groups</a></li>
|
||||
<li class="active">{{membership.group.name}}</li>
|
||||
</ul>
|
||||
<!-- END BREADCRUMB -->
|
||||
<div>
|
||||
<!-- START BREADCRUMB -->
|
||||
<ul class="breadcrumb">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/groups">Groups</a></li>
|
||||
<li class="active">{{membership.group.name}}</li>
|
||||
</ul>
|
||||
<!-- END BREADCRUMB -->
|
||||
|
||||
<!-- PAGE TITLE -->
|
||||
<div class="page-title">
|
||||
<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>
|
||||
<!-- PAGE TITLE -->
|
||||
<div class="page-title">
|
||||
<h3><span class="fa fa-group"></span> Group {{membership.group.name}}</h3>
|
||||
</div>
|
||||
<!-- END PAGE TITLE -->
|
||||
|
||||
<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/>
|
||||
<!-- 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>
|
||||
|
||||
<!-- START VERTICAL TABS -->
|
||||
<div class="panel panel-default panel-tabs">
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<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 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 class="panel-body">
|
||||
<p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p>
|
||||
<p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p>
|
||||
<table class="table datatable_simple group-members" ng-if="membership.members.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Name</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Group Manager</th>
|
||||
<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>
|
||||
</form>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p id="no-group-list" ng-if="!membershipLoaded">Loading members...</p>
|
||||
<p id="no-group-list" ng-if="!membership.members.length && membershipLoaded">You don't have any members yet.</p>
|
||||
<table class="table datatable_simple group-members" ng-if="membership.members.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User Name</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Group Manager</th>
|
||||
<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 class="tab-pane" id="tab2">
|
||||
<!-- START SIMPLE DATATABLE -->
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">All Group Changes {{ getChangePageTitle() }}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default" ng-click="refreshGroupChanges()"><span class="fa fa-refresh"></span> Refresh</button>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
|
||||
<table id="changeDataTable" class="table table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Group Change ID</th>
|
||||
<th>Change Type</th>
|
||||
<th>Change Message</th>
|
||||
<th>Change Info</th>
|
||||
<th>User</th>
|
||||
</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>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- END VERTICAL TABS -->
|
||||
|
||||
</div>
|
||||
<!-- PAGE CONTENT WRAPPER -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAGE CONTENT WRAPPER -->
|
||||
</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=" "
|
||||
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=" "
|
||||
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 = {
|
||||
|
@ -26,12 +26,12 @@
|
||||
<!-- START VERTICAL TABS -->
|
||||
<div class="panel panel-default panel-tabs">
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<li class="active"><a data-toggle="tab" ng-click="myGroups()">My Groups</a></li>
|
||||
<li><a data-toggle="tab" ng-click="allGroups()">All Groups</a></li>
|
||||
<li class="active"><a href="#myGroups" data-toggle="tab">My Groups</a></li>
|
||||
<li><a id="tab2-button" href="#allGroups" data-toggle="tab">All Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<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="col-md-12">
|
||||
|
||||
@ -66,7 +66,22 @@
|
||||
<div id="group-list" class="panel-body">
|
||||
<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="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">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -96,6 +111,118 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
</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>
|
||||
<!-- END SIMPLE DATATABLE -->
|
||||
|
@ -41,7 +41,8 @@
|
||||
<ul class="nav nav-tabs bar_tabs">
|
||||
<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 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>
|
||||
<div class="panel-body tab-content">
|
||||
|
||||
@ -54,6 +55,9 @@
|
||||
<div class="tab-pane" id="tab3">
|
||||
@changeHistory(request)
|
||||
</div>
|
||||
<div class="tab-pane" id="tab4" ng-controller="ManageZonesController">
|
||||
@zoneChangeHistory(request)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END VERTICAL TABS -->
|
||||
|
@ -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 -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -77,8 +77,8 @@
|
||||
<p ng-if="hasZones && zonesLoaded && !zones.length">No zones match the search criteria.</p>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span>
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('myZones')" ng-click="prevPageMyZones()">Previous</a>
|
||||
@ -128,8 +128,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("myZones") }}</span>
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("myZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<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>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span>
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>
|
||||
@ -245,8 +245,8 @@
|
||||
</table>
|
||||
|
||||
<!-- PAGINATION -->
|
||||
<div class="dataTables_paginate vinyldns_zones_paginate">
|
||||
<span class="vinyldns_zones_page_number">{{ getZonesPageNumber("allZones") }}</span>
|
||||
<div class="dataTables_paginate vinyldns_paginate">
|
||||
<span class="vinyldns_page_number">{{ getZonesPageNumber("allZones") }}</span>
|
||||
<ul class="pagination">
|
||||
<li class="paginate_button previous">
|
||||
<a ng-if="prevPageEnabled('allZones')" ng-click="prevPageAllZones()">Previous</a>
|
||||
|
@ -30,6 +30,7 @@ GET /api/zones @controllers.VinylDNS.getZones
|
||||
GET /api/zones/backendids @controllers.VinylDNS.getBackendIds
|
||||
GET /api/zones/:id @controllers.VinylDNS.getZone(id: 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
|
||||
PUT /api/zones/:id @controllers.VinylDNS.updateZone(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/: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
|
||||
PUT /api/groups/:gid @controllers.VinylDNS.updateGroup(gid: String)
|
||||
DELETE /api/groups/:gid @controllers.VinylDNS.deleteGroup(gid: String)
|
||||
|
@ -434,12 +434,12 @@ input[type="file"] {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.vinyldns_zones_paginate {
|
||||
.vinyldns_paginate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.vinyldns_zones_page_number {
|
||||
.vinyldns_page_number {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
@ -14,19 +14,25 @@
|
||||
* 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
|
||||
angular.element('#modal_new_group').one('hide.bs.modal', function () {
|
||||
$scope.closeModal();
|
||||
});
|
||||
|
||||
$scope.groups = {items: []};
|
||||
$scope.allGroup = {items: []};
|
||||
$scope.groupsLoaded = false;
|
||||
$scope.allGroupsLoaded = false;
|
||||
$scope.alerts = [];
|
||||
$scope.ignoreAccess = false;
|
||||
$scope.hasGroups = false; // Re-assigned each time groups are fetched without a query
|
||||
$scope.hasGroups = false;
|
||||
$scope.query = "";
|
||||
|
||||
// Paging status for group sets
|
||||
var groupsPaging = pagingService.getNewPagingParams(100);
|
||||
var allGroupsPaging = pagingService.getNewPagingParams(100);
|
||||
|
||||
function handleError(error, type) {
|
||||
var alert = utilityService.failure(error, type);
|
||||
$scope.alerts.push(alert);
|
||||
@ -68,7 +74,7 @@ angular.module('controller.groups', []).controller('GroupsController', function
|
||||
$("#group-search-text").autocomplete({
|
||||
source: function( request, response ) {
|
||||
$.ajax({
|
||||
url: "/api/groups?maxItems=1500&abridged=true",
|
||||
url: "/api/groups?maxItems=100&abridged=true",
|
||||
dataType: "json",
|
||||
data: {groupNameFilter: request.term, ignoreAccess: $scope.ignoreAccess},
|
||||
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 () {
|
||||
function success(result) {
|
||||
$log.log('getGroups:refresh-success', result);
|
||||
//update groups
|
||||
$scope.groups.items = result.groups;
|
||||
$scope.groupsLoaded = true;
|
||||
if (!$scope.query.length) {
|
||||
$scope.hasGroups = $scope.groups.items.length > 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
groupsPaging = pagingService.resetPaging(groupsPaging);
|
||||
allGroupsPaging = pagingService.resetPaging(allGroupsPaging);
|
||||
|
||||
getGroupsAbridged($scope.ignoreAccess)
|
||||
.then(success)
|
||||
groupsService
|
||||
.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) {
|
||||
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
|
||||
$scope.haveNoGroups = function (groupLength) {
|
||||
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
|
||||
$scope.searchCriteria = function (groupLength) {
|
||||
if ($scope.groupsLoaded && !groupLength && $scope.query.length != "") {
|
||||
if (!groupLength && $scope.query.length != "") {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
@ -336,4 +331,115 @@ angular.module('controller.groups', []).controller('GroupsController', function
|
||||
.then(profileSuccess, 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);
|
||||
});
|
||||
|
@ -19,14 +19,16 @@ describe('Controller: GroupsController', function () {
|
||||
module('ngMock'),
|
||||
module('service.groups'),
|
||||
module('service.profile'),
|
||||
module('service.utility')
|
||||
module('service.utility'),
|
||||
module('service.paging'),
|
||||
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.groupsService = groupsService;
|
||||
this.utilityService = utilityService;
|
||||
this.q = $q;
|
||||
this.pagingService = pagingService;
|
||||
|
||||
profileService.getAuthenticatedUserData = function() {
|
||||
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.mockSuccessAlert = 'success';
|
||||
@ -64,7 +75,7 @@ describe('Controller: GroupsController', function () {
|
||||
this.scope.refresh();
|
||||
this.scope.$digest();
|
||||
|
||||
expect(getGroups.calls.count()).toBe(1);
|
||||
expect(getGroups.calls.count()).toBe(2);
|
||||
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.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]);
|
||||
});
|
||||
});
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
angular.module('controller.manageZones', [])
|
||||
.controller('ManageZonesController', function ($scope, $timeout, $log, recordsService, zonesService, groupsService,
|
||||
profileService, utilityService) {
|
||||
profileService, utilityService, pagingService) {
|
||||
|
||||
groupsService.getGroupsStored()
|
||||
.then(function (results) {
|
||||
@ -38,6 +38,7 @@ angular.module('controller.manageZones', [])
|
||||
|
||||
$scope.alerts = [];
|
||||
$scope.zoneInfo = {};
|
||||
$scope.zoneChanges = {};
|
||||
$scope.updateZoneInfo = {};
|
||||
$scope.manageZoneState = {
|
||||
UPDATE: 0,
|
||||
@ -60,7 +61,8 @@ angular.module('controller.manageZones', [])
|
||||
CREATE: 0,
|
||||
UPDATE: 1,
|
||||
CONFIRM_UPDATE: 2,
|
||||
CONFIRM_DELETE: 3
|
||||
CONFIRM_DELETE: 3,
|
||||
VIEW_DETAILS: 4
|
||||
};
|
||||
$scope.aclModalParams = {
|
||||
readOnly: {
|
||||
@ -74,6 +76,8 @@ angular.module('controller.manageZones', [])
|
||||
};
|
||||
$scope.aclRecordTypes = ['A', 'AAAA', 'CNAME', 'DS', 'MX', 'NS', 'PTR', 'SRV', 'NAPTR', 'SSHFP', 'TXT'];
|
||||
|
||||
var zoneHistoryPaging = pagingService.getNewPagingParams(100);
|
||||
|
||||
/**
|
||||
* Zone modal control functions
|
||||
*/
|
||||
@ -276,6 +280,7 @@ angular.module('controller.manageZones', [])
|
||||
$scope.updateZoneInfo.hiddenTransferKey = '';
|
||||
$scope.currentManageZoneState = $scope.manageZoneState.UPDATE;
|
||||
$scope.refreshAclRuleDisplay();
|
||||
$scope.refreshZoneChange();
|
||||
}
|
||||
return recordsService
|
||||
.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.aclRules = [];
|
||||
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
|
||||
*/
|
||||
|
@ -22,16 +22,18 @@ describe('Controller: ManageZonesController', function () {
|
||||
module('service.utility'),
|
||||
module('service.zones'),
|
||||
module('service.profile'),
|
||||
module('service.paging'),
|
||||
module('controller.manageZones')
|
||||
});
|
||||
beforeEach(inject(function ($rootScope, $controller, $q, groupsService, recordsService, zonesService,
|
||||
profileService) {
|
||||
profileService, pagingService) {
|
||||
this.rootScope = $rootScope;
|
||||
this.scope = $rootScope.$new();
|
||||
this.groupsService = groupsService;
|
||||
this.zonesService = zonesService;
|
||||
this.recordsService = recordsService;
|
||||
this.profileService = profileService;
|
||||
this.pagingService = pagingService;
|
||||
this.q = $q;
|
||||
this.groupsService.getGroups = function () {
|
||||
return $q.when({
|
||||
@ -205,11 +207,14 @@ describe('Controller: ManageZonesController', function () {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var getZone = spyOn(this.recordsService, 'getZone')
|
||||
.and.stub()
|
||||
.and.returnValue(this.q.when(mockResponse));
|
||||
var refreshAclRuleDisplay = spyOn(this.scope, 'refreshAclRuleDisplay')
|
||||
.and.stub();
|
||||
var refreshZoneChange = spyOn(this.scope, 'refreshZoneChange')
|
||||
.and.stub();
|
||||
this.scope.currentManageZoneState = this.scope.manageZoneState.CONFIRM_UPDATE;
|
||||
this.scope.updateZoneInfo.hiddenKey = 'some key';
|
||||
this.scope.updateZoneInfo.hiddenTransferKey = 'some key';
|
||||
@ -217,6 +222,7 @@ describe('Controller: ManageZonesController', function () {
|
||||
this.scope.$digest();
|
||||
expect(getZone.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.updateZoneInfo. adminGroupId).toEqual('id101112');
|
||||
expect(this.scope.updateZoneInfo.hiddenKey).toEqual('');
|
||||
@ -554,4 +560,99 @@ describe('Controller: ManageZonesController', function () {
|
||||
expect(toDisplayAclRule.calls.count()).toBe(3);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -14,13 +14,35 @@
|
||||
* 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) {
|
||||
|
||||
$scope.membership = { members: [], group: {} };
|
||||
$scope.membershipLoaded = false;
|
||||
$scope.alerts = [];
|
||||
$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) {
|
||||
var alert = utilityService.failure(error, type);
|
||||
@ -207,8 +229,113 @@ angular.module('controller.membership', []).controller('MembershipController', f
|
||||
|
||||
$scope.resetNewMemberData();
|
||||
$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);
|
||||
});
|
||||
|
@ -20,14 +20,16 @@ describe('Controller: MembershipController', function () {
|
||||
module('service.groups'),
|
||||
module('service.profile'),
|
||||
module('service.utility'),
|
||||
module('service.paging'),
|
||||
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.scope = $rootScope.$new();
|
||||
this.groupsService = groupsService;
|
||||
this.profileService = profileService;
|
||||
this.utilityService = utilityService;
|
||||
this.pagingService = pagingService;
|
||||
this.q = $q;
|
||||
var mockGroup = {
|
||||
data: {
|
||||
@ -68,6 +70,47 @@ describe('Controller: MembershipController', function () {
|
||||
this.groupsService.getGroupMemberList = function() {
|
||||
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.mockSuccessAlert = "success";
|
||||
@ -401,4 +444,169 @@ describe('Controller: MembershipController', function () {
|
||||
expect(this.scope.membership.members).toEqual(expectedMembership);
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
@ -53,7 +53,7 @@ angular.module('controller.zones', [])
|
||||
$scope.currentZone.transferConnection = {};
|
||||
};
|
||||
|
||||
groupsService.getGroupsAbridged(true, "").then(function (results) {
|
||||
groupsService.getGroups(true, "").then(function (results) {
|
||||
if (results.data) {
|
||||
// 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);
|
||||
|
@ -39,7 +39,7 @@ describe('Controller: ZonesController', function () {
|
||||
profileService.getAuthenticatedUserData = function() {
|
||||
return $q.when({data: {id: "userId"}});
|
||||
};
|
||||
groupsService.getGroupsAbridged = function () {
|
||||
groupsService.getGroups = function () {
|
||||
return $q.when({
|
||||
data: {
|
||||
groups: [{id: "all my groups", members: [{id: "userId"}]}]
|
||||
|
@ -56,7 +56,7 @@ angular.module('service.groups', [])
|
||||
|
||||
this.getGroupMemberList = function (uuid) {
|
||||
var url = '/api/groups/' + uuid + '/members';
|
||||
url = this.urlBuilder(url, { maxItems: 1000 });
|
||||
url = this.urlBuilder(url, { maxItems: 100 });
|
||||
return $http.get(url);
|
||||
};
|
||||
|
||||
@ -75,7 +75,7 @@ angular.module('service.groups', [])
|
||||
query = null;
|
||||
}
|
||||
var params = {
|
||||
"maxItems": 1500,
|
||||
"maxItems": 100,
|
||||
"groupNameFilter": query,
|
||||
"ignoreAccess": ignoreAccess
|
||||
};
|
||||
@ -84,12 +84,13 @@ angular.module('service.groups', [])
|
||||
return $http.get(url);
|
||||
};
|
||||
|
||||
this.getGroupsAbridged = function (ignoreAccess, query) {
|
||||
this.getGroupsAbridged = function (limit, startFrom, ignoreAccess, query) {
|
||||
if (query == "") {
|
||||
query = null;
|
||||
}
|
||||
var params = {
|
||||
"maxItems": 1500,
|
||||
"maxItems": limit,
|
||||
"startFrom": startFrom,
|
||||
"groupNameFilter": query,
|
||||
"ignoreAccess": ignoreAccess,
|
||||
"abridged": true
|
||||
@ -105,6 +106,12 @@ angular.module('service.groups', [])
|
||||
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 () {
|
||||
if (_refreshMyGroups || _myGroupsPromise == undefined) {
|
||||
_myGroupsPromise = this.getGroups().then(
|
||||
|
@ -26,6 +26,10 @@ angular.module('service.profile', [])
|
||||
return $http.get('/api/users/lookupuser/' + username);
|
||||
}
|
||||
|
||||
this.getUserDataById = function(userId){
|
||||
return $http.get('/api/users/' + userId);
|
||||
}
|
||||
|
||||
this.regenerateCredentials = function(){
|
||||
return $http.post('/regenerate-creds', {}, {headers: utilityService.getCsrfHeader()});
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ describe('Service: profileService', function () {
|
||||
expect(this.profileService.getUserDataByUsername).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have getUserDataById method', function () {
|
||||
expect(this.profileService.getUserDataByUsername).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have regenerateCredentials method', function () {
|
||||
expect(this.profileService.regenerateCredentials()).toBeDefined();
|
||||
});
|
||||
@ -119,6 +123,39 @@ describe('Service: profileService', function () {
|
||||
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) {
|
||||
var url = '/regenerate-creds';
|
||||
this.$httpBackend.whenRoute('POST', url).respond(400);
|
||||
|
@ -43,6 +43,15 @@ angular.module('service.zones', [])
|
||||
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() {
|
||||
var url = "/api/zones/backendids";
|
||||
return $http.get(url);
|
||||
|
@ -35,6 +35,15 @@ describe('Service: zoneService', function () {
|
||||
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) {
|
||||
this.$httpBackend.expectPOST('/api/zones').respond('zone sent');
|
||||
this.zonesService.sendZone('zone payload')
|
||||
|
@ -145,6 +145,48 @@ trait TestApplicationData { this: Mockito =>
|
||||
| }
|
||||
""".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(
|
||||
s"""{
|
||||
| "id": "ringbearer-group-uuid",
|
||||
@ -196,6 +238,25 @@ trait TestApplicationData { this: Mockito =>
|
||||
| }
|
||||
""".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"""{
|
||||
| "name": "hobbits",
|
||||
| "email": "hobbitAdmin@shire.me",
|
||||
|
@ -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 {
|
||||
"return ok with no content (204) when delete is successful" in new WithApplication(app) {
|
||||
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 {
|
||||
"return ok (200) if the zone is found" in new WithApplication(app) {
|
||||
val client = MockWS {
|
||||
|
Loading…
x
Reference in New Issue
Block a user