<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12369] twext/trunk/twext/who/ldap</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12369">12369</a></dd>
<dt>Author</dt> <dd>wsanchez@apple.com</dd>
<dt>Date</dt> <dd>2014-01-16 19:02:21 -0800 (Thu, 16 Jan 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>Change the field name map so that it maps to multiple LDAP attributes.
Change the record type map so that it maps to multiple LDAP object classes.
Much fallout ensues.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#twexttrunktwextwholdap_servicepy">twext/trunk/twext/who/ldap/_service.py</a></li>
<li><a href="#twexttrunktwextwholdap_utilpy">twext/trunk/twext/who/ldap/_util.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_servicepy">twext/trunk/twext/who/ldap/test/test_service.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_utilpy">twext/trunk/twext/who/ldap/test/test_util.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="twexttrunktwextwholdap_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_service.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_service.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_service.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -49,21 +49,21 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-# Maps field name -> LDAP attribute name
-DEFAULT_FIELDNAME_MAP = {
- BaseFieldName.guid: LDAPAttribute.generatedUUID.value,
- BaseFieldName.recordType: LDAPAttribute.objectClass.value,
- BaseFieldName.shortNames: LDAPAttribute.uid.value,
- BaseFieldName.fullNames: LDAPAttribute.cn.value,
- BaseFieldName.emailAddresses: LDAPAttribute.mail.value,
- BaseFieldName.password: LDAPAttribute.userPassword.value,
</del><ins>+# Maps field name -> LDAP attribute names
+DEFAULT_FIELDNAME_ATTRIBUTE_MAP = {
+ BaseFieldName.guid: (LDAPAttribute.generatedUUID.value,),
+ BaseFieldName.recordType: (LDAPAttribute.objectClass.value,),
+ BaseFieldName.shortNames: (LDAPAttribute.uid.value,),
+ BaseFieldName.fullNames: (LDAPAttribute.cn.value,),
+ BaseFieldName.emailAddresses: (LDAPAttribute.mail.value,),
+ BaseFieldName.password: (LDAPAttribute.userPassword.value,),
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx">
</span><del>-# Maps record type -> LDAP object class name
-DEFAULT_RECORDTYPE_MAP = {
- BaseRecordType.user: LDAPObjectClass.person.value,
- BaseRecordType.group: LDAPObjectClass.groupOfNames.value,
</del><ins>+# Maps record type -> LDAP object class names
+DEFAULT_RECORDTYPE_OBJECTCLASS_MAP = {
+ BaseRecordType.user: (LDAPObjectClass.person.value,),
+ BaseRecordType.group: (LDAPObjectClass.groupOfNames.value,),
</ins><span class="cx"> }
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -147,8 +147,8 @@
</span><span class="cx"> tlsCACertificateFile=None,
</span><span class="cx"> tlsCACertificateDirectory=None,
</span><span class="cx"> useTLS=False,
</span><del>- fieldNameToAttributeMap=DEFAULT_FIELDNAME_MAP,
- recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_MAP,
</del><ins>+ fieldNameToAttributeMap=DEFAULT_FIELDNAME_ATTRIBUTE_MAP,
+ recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</ins><span class="cx"> uidField=BaseFieldName.uid,
</span><span class="cx"> _debug=False,
</span><span class="cx"> ):
</span><span class="lines">@@ -178,12 +178,12 @@
</span><span class="cx"> @param fieldNameToAttributeMap: A mapping of field names to LDAP
</span><span class="cx"> attribute names.
</span><span class="cx"> @type fieldNameToAttributeMap: mapping with L{NamedConstant} keys and
</span><del>- L{unicode} values
</del><ins>+ sequence of L{unicode} values
</ins><span class="cx">
</span><span class="cx"> @param recordTypeToObjectClassMap: A mapping of record types to LDAP
</span><span class="cx"> object classes.
</span><span class="cx"> @type recordTypeToObjectClassMap: mapping with L{NamedConstant} keys
</span><del>- and L{unicode} values
</del><ins>+ and sequence of L{unicode} values
</ins><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> self.url = url
</span><span class="lines">@@ -208,24 +208,28 @@
</span><span class="cx"> else:
</span><span class="cx"> self._debug = None
</span><span class="cx">
</span><del>- def reverseDict(source):
</del><ins>+ def reverseDict(sourceName, source):
</ins><span class="cx"> new = {}
</span><span class="cx">
</span><del>- for k, v in source.iteritems():
- if v in new:
- raise LDAPConfigurationError(
- u"Field name map has duplicate values: {0}".format(v)
- )
- new[v] = k
</del><ins>+ for key, values in source.iteritems():
+ for value in values:
+ if value in new:
+ raise LDAPConfigurationError(
+ u"{0} map has duplicate values: {1}"
+ .format(sourceName, value)
+ )
+ new[value] = key
</ins><span class="cx">
</span><span class="cx"> return new
</span><span class="cx">
</span><span class="cx"> self._fieldNameToAttributeMap = fieldNameToAttributeMap
</span><del>- self._attributeToFieldNameMap = reverseDict(fieldNameToAttributeMap)
</del><ins>+ self._attributeToFieldNameMap = reverseDict(
+ "Field name", fieldNameToAttributeMap
+ )
</ins><span class="cx">
</span><span class="cx"> self._recordTypeToObjectClassMap = recordTypeToObjectClassMap
</span><span class="cx"> self._objectClassToRecordTypeMap = reverseDict(
</span><del>- recordTypeToObjectClassMap
</del><ins>+ "Record type", recordTypeToObjectClassMap
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> self._uidField = uidField
</span><span class="lines">@@ -317,23 +321,28 @@
</span><span class="cx"> # we are using to determine the UID of the record.
</span><span class="cx">
</span><span class="cx"> uidField = self.fieldName.uid
</span><del>- uidAttribute = self._fieldNameToAttributeMap[self._uidField]
</del><ins>+ uidAttribute = self._fieldNameToAttributeMap[self._uidField][0]
</ins><span class="cx">
</span><span class="cx"> recordTypeField = self.fieldName.recordType
</span><del>- recordTypeAttribute = (
</del><ins>+ recordTypeAttributes = (
</ins><span class="cx"> self._fieldNameToAttributeMap[self.fieldName.recordType]
</span><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> for dn, recordData in reply:
</span><span class="cx">
</span><del>- if recordTypeAttribute not in recordData:
- self.log.debug(
- "Ignoring LDAP record data with no record type attribute "
- "{source.fieldName.recordType!r}: {recordData!r}",
- self=self, recordData=recordData
- )
- continue
</del><ins>+ # Attributes used to determine the record type are required, since
+ # record type is very much required.
</ins><span class="cx">
</span><ins>+ for recordTypeAttribute in recordTypeAttributes:
+ if recordTypeAttribute not in recordData:
+ self.log.debug(
+ "Ignoring LDAP record data without record type "
+ "attribute {attribute!r}: "
+ "{recordData!r}",
+ attribute=recordTypeAttribute, recordData=recordData,
+ )
+ continue
+
</ins><span class="cx"> # Make a dict of fields -> values from the incoming dict of
</span><span class="cx"> # attributes -> values.
</span><span class="cx">
</span><span class="lines">@@ -351,7 +360,7 @@
</span><span class="cx"> self.log.debug(
</span><span class="cx"> "Ignoring LDAP record data with no UID attribute "
</span><span class="cx"> "{source._uidField!r}: {recordData!r}",
</span><del>- self=self, recordData=recordData
</del><ins>+ recordData=recordData
</ins><span class="cx"> )
</span><span class="cx"> continue
</span><span class="cx">
</span></span></pre></div>
<a id="twexttrunktwextwholdap_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_util.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_util.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_util.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -21,12 +21,37 @@
</span><span class="cx"> MatchExpression, MatchFlags,
</span><span class="cx"> )
</span><span class="cx"> from ..util import iterFlags
</span><del>-from ._constants import LDAPMatchType
</del><ins>+from ._constants import LDAPOperand, LDAPMatchType, LDAPMatchFlags
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><ins>+def ldapQueryStringFromQueryStrings(operand, queryStrings):
+ """
+ Combines LDAP query strings into a single query string.
+
+ @param operand: An LDAP operand (C{u"&"} or C{u"|"}).
+ @type operand: L{unicode}
+
+ @param queryStrings: LDAP query strings.
+ @type queryStrings: iterable of L{unicode}
+ """
+ if len(queryStrings) == 1:
+ return queryStrings[0]
+
+ elif len(queryStrings) > 1:
+ queryTokens = []
+ queryTokens.append(u"(")
+ queryTokens.append(operand)
+ queryTokens.extend(queryStrings)
+ queryTokens.append(u")")
+ return u"".join(queryTokens)
+
+ else:
+ return u""
+
+
</ins><span class="cx"> def ldapQueryStringFromMatchExpression(
</span><del>- expression, fieldNameMap, recordTypeMap
</del><ins>+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx"> """
</span><span class="cx"> Generates an LDAP query string from a match expression.
</span><span class="lines">@@ -34,21 +59,24 @@
</span><span class="cx"> @param expression: A match expression.
</span><span class="cx"> @type expression: L{MatchExpression}
</span><span class="cx">
</span><del>- @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
- names.
- @type fieldNameMap: L{dict}
</del><ins>+ @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+ attribute names.
+ @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+ of L{unicode} values.
</ins><span class="cx">
</span><del>- @param recordTypeMap: A mapping from L{RecordType}s to native LDAP object
- class names.
- @type recordTypeMap: L{dict}
</del><ins>+ @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+ LDAP object class names.
+ @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+ sequence of L{unicode} values.
</ins><span class="cx">
</span><span class="cx"> @return: An LDAP query string.
</span><del>- @rtype: C{unicode}
</del><ins>+ @rtype: L{unicode}
</ins><span class="cx">
</span><span class="cx"> @raises QueryNotSupportedError: If the expression's match type is unknown,
</span><span class="cx"> or if the expresion references an unknown field name (meaning a field
</span><del>- name not in C{fieldNameMap}).
</del><ins>+ name not in C{fieldNameToAttributeMap}).
</ins><span class="cx"> """
</span><ins>+
</ins><span class="cx"> matchType = LDAPMatchType.fromMatchType(expression.matchType)
</span><span class="cx"> if matchType is None:
</span><span class="cx"> raise QueryNotSupportedError(
</span><span class="lines">@@ -58,7 +86,7 @@
</span><span class="cx"> flags = tuple(iterFlags(expression.flags))
</span><span class="cx">
</span><span class="cx"> if MatchFlags.NOT in flags:
</span><del>- notOp = u"!"
</del><ins>+ notOp = LDAPMatchFlags.NOT.value
</ins><span class="cx"> else:
</span><span class="cx"> notOp = u""
</span><span class="cx">
</span><span class="lines">@@ -71,7 +99,7 @@
</span><span class="cx">
</span><span class="cx"> fieldName = expression.fieldName
</span><span class="cx"> try:
</span><del>- attribute = fieldNameMap[fieldName]
</del><ins>+ attributes = fieldNameToAttributeMap[fieldName]
</ins><span class="cx"> except KeyError:
</span><span class="cx"> raise QueryNotSupportedError(
</span><span class="cx"> "Unmapped field name: {0}".format(expression.fieldName)
</span><span class="lines">@@ -79,23 +107,49 @@
</span><span class="cx">
</span><span class="cx"> if fieldName is FieldName.recordType:
</span><span class="cx"> try:
</span><del>- value = recordTypeMap[expression.fieldValue]
</del><ins>+ values = recordTypeToObjectClassMap[expression.fieldValue]
</ins><span class="cx"> except KeyError:
</span><span class="cx"> raise QueryNotSupportedError(
</span><span class="cx"> "Unmapped record type: {0}".format(expression.fieldValue)
</span><span class="cx"> )
</span><span class="cx"> else:
</span><del>- value = unicode(expression.fieldValue)
</del><ins>+ values = (unicode(expression.fieldValue),)
</ins><span class="cx">
</span><del>- value = value.translate(LDAP_QUOTING_TABLE) # Escape special chars
</del><ins>+ # Escape special LDAP query characters
+ values = [value.translate(LDAP_QUOTING_TABLE) for value in values]
+ del value # Symbol used below; ensure non-reuse of data
</ins><span class="cx">
</span><del>- return matchType.queryString.format(
- notOp=notOp, attribute=attribute, value=value
- )
</del><ins>+ # Compose an query using each of the LDAP attributes cooresponding to the
+ # target field name.
</ins><span class="cx">
</span><ins>+ if notOp:
+ operand = LDAPOperand.AND.value
+ else:
+ operand = LDAPOperand.OR.value
</ins><span class="cx">
</span><ins>+ if notOp:
+ valueOperand = LDAPOperand.OR.value
+ else:
+ valueOperand = LDAPOperand.AND.value
+
+ queryStrings = [
+ ldapQueryStringFromQueryStrings(
+ valueOperand,
+ [
+ matchType.queryString.format(
+ notOp=notOp, attribute=attribute, value=value
+ )
+ for value in values
+ ]
+ )
+ for attribute in attributes
+ ]
+
+ return ldapQueryStringFromQueryStrings(operand, queryStrings)
+
+
</ins><span class="cx"> def ldapQueryStringFromCompoundExpression(
</span><del>- expression, fieldNameMap, recordTypeMap
</del><ins>+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx"> """
</span><span class="cx"> Generates an LDAP query string from a compound expression.
</span><span class="lines">@@ -103,67 +157,73 @@
</span><span class="cx"> @param expression: A compound expression.
</span><span class="cx"> @type expression: L{MatchExpression}
</span><span class="cx">
</span><del>- @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
- names.
- @type fieldNameMap: L{dict}
</del><ins>+ @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+ attribute names.
+ @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+ of L{unicode} values.
</ins><span class="cx">
</span><ins>+ @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+ LDAP object class names.
+ @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+ sequence of L{unicode} values.
+
</ins><span class="cx"> @return: An LDAP query string.
</span><del>- @rtype: C{unicode}
</del><ins>+ @rtype: L{unicode}
</ins><span class="cx">
</span><span class="cx"> @raises QueryNotSupportedError: If any sub-expression cannot be converted
</span><span class="cx"> to an LDAP query.
</span><span class="cx"> """
</span><del>- queryTokens = []
</del><ins>+ if expression.operand is Operand.AND:
+ operand = LDAPOperand.AND.value
</ins><span class="cx">
</span><del>- if len(expression.expressions) > 1:
- queryTokens.append(u"(")
</del><ins>+ elif expression.operand is Operand.OR:
+ operand = LDAPOperand.OR.value
</ins><span class="cx">
</span><del>- if expression.operand is Operand.AND:
- queryTokens.append(u"&")
- else:
- queryTokens.append(u"|")
-
- for subExpression in expression.expressions:
- queryTokens.append(
- ldapQueryStringFromExpression(
- subExpression, fieldNameMap, recordTypeMap
- )
</del><ins>+ queryStrings = [
+ ldapQueryStringFromExpression(
+ subExpression,
+ fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> )
</span><ins>+ for subExpression in expression.expressions
+ ]
</ins><span class="cx">
</span><del>- if len(expression.expressions) > 1:
- queryTokens.append(u")")
</del><ins>+ return ldapQueryStringFromQueryStrings(operand, queryStrings)
</ins><span class="cx">
</span><del>- return u"".join(queryTokens)
</del><span class="cx">
</span><del>-
</del><span class="cx"> def ldapQueryStringFromExpression(
</span><del>- expression, fieldNameMap, recordTypeMap
</del><ins>+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> ):
</span><span class="cx"> """
</span><span class="cx"> Converts an expression into an LDAP query string.
</span><span class="cx">
</span><del>- @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
- names.
- @type fieldNameMap: L{dict}
-
</del><span class="cx"> @param expression: An expression.
</span><span class="cx"> @type expression: L{MatchExpression} or L{CompoundExpression}
</span><span class="cx">
</span><del>- @return: A native OpenDirectory query string
- @rtype: C{unicode}
</del><ins>+ @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+ attribute names.
+ @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+ of L{unicode} values.
</ins><span class="cx">
</span><ins>+ @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+ LDAP object class names.
+ @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+ sequence of L{unicode} values.
+
+ @return: An LDAP query string.
+ @rtype: L{unicode}
+
</ins><span class="cx"> @raises QueryNotSupportedError: If the expression cannot be converted to an
</span><span class="cx"> LDAP query.
</span><span class="cx"> """
</span><span class="cx">
</span><span class="cx"> if isinstance(expression, MatchExpression):
</span><span class="cx"> return ldapQueryStringFromMatchExpression(
</span><del>- expression, fieldNameMap, recordTypeMap
</del><ins>+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> if isinstance(expression, CompoundExpression):
</span><span class="cx"> return ldapQueryStringFromCompoundExpression(
</span><del>- expression, fieldNameMap, recordTypeMap
</del><ins>+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</ins><span class="cx"> )
</span><span class="cx">
</span><span class="cx"> raise QueryNotSupportedError(
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_service.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_service.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_service.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -20,6 +20,8 @@
</span><span class="cx"> LDAP directory service tests.
</span><span class="cx"> """
</span><span class="cx">
</span><ins>+from itertools import chain
+
</ins><span class="cx"> import ldap
</span><span class="cx"> from mockldap import MockLdap
</span><span class="cx">
</span><span class="lines">@@ -31,7 +33,7 @@
</span><span class="cx">
</span><span class="cx"> from ...idirectory import QueryNotSupportedError, FieldName as BaseFieldName
</span><span class="cx"> from .._service import (
</span><del>- DEFAULT_FIELDNAME_MAP, DEFAULT_RECORDTYPE_MAP,
</del><ins>+ DEFAULT_FIELDNAME_ATTRIBUTE_MAP, DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</ins><span class="cx"> LDAPBindAuthError,
</span><span class="cx"> DirectoryService, DirectoryRecord,
</span><span class="cx"> )
</span><span class="lines">@@ -45,8 +47,8 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_MAP)
-TEST_FIELDNAME_MAP[BaseFieldName.uid] = u"__who_uid__"
</del><ins>+TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_ATTRIBUTE_MAP)
+TEST_FIELDNAME_MAP[BaseFieldName.uid] = (u"__who_uid__",)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><span class="lines">@@ -236,14 +238,16 @@
</span><span class="cx"> return unicode(obj)
</span><span class="cx">
</span><span class="cx"> def tuplify(record, fieldName):
</span><del>- name = TEST_FIELDNAME_MAP.get(fieldName, fieldName.name)
-
</del><span class="cx"> if fieldName is BaseFieldName.recordType:
</span><del>- value = DEFAULT_RECORDTYPE_MAP[record.fields[fieldName]]
</del><ins>+ values = DEFAULT_RECORDTYPE_OBJECTCLASS_MAP[
+ record.fields[fieldName]
+ ]
</ins><span class="cx"> else:
</span><del>- value = toUnicode(record.fields[fieldName])
</del><ins>+ values = (toUnicode(record.fields[fieldName]),)
</ins><span class="cx">
</span><del>- return (name, value)
</del><ins>+ for name in TEST_FIELDNAME_MAP.get(fieldName, fieldName.name):
+ for value in values:
+ yield (name, value)
</ins><span class="cx">
</span><span class="cx"> for records in service.index[service.fieldName.uid].itervalues():
</span><span class="cx"> for record in records:
</span><span class="lines">@@ -253,11 +257,11 @@
</span><span class="cx"> dc1=dc1, dc0=dc0
</span><span class="cx"> )
</span><span class="cx">
</span><del>- recordData = dict(
- tuplify(record, fieldName)
</del><ins>+ recordData = dict(chain(*(
+ list(tuplify(record, fieldName))
</ins><span class="cx"> for fieldName in service.fieldName.iterconstants()
</span><span class="cx"> if fieldName in record.fields
</span><del>- )
</del><ins>+ )))
</ins><span class="cx">
</span><span class="cx"> data[dn] = recordData
</span><span class="cx">
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_util.py (12368 => 12369)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_util.py        2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_util.py        2014-01-17 03:02:21 UTC (rev 12369)
</span><span class="lines">@@ -24,8 +24,10 @@
</span><span class="cx"> from ...expression import (
</span><span class="cx"> CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
</span><span class="cx"> )
</span><ins>+from .._constants import LDAPOperand
</ins><span class="cx"> from .._service import DirectoryService
</span><span class="cx"> from .._util import (
</span><ins>+ ldapQueryStringFromQueryStrings,
</ins><span class="cx"> ldapQueryStringFromMatchExpression,
</span><span class="cx"> ldapQueryStringFromCompoundExpression,
</span><span class="cx"> ldapQueryStringFromExpression,
</span><span class="lines">@@ -51,7 +53,10 @@
</span><span class="cx"> but we don't care for these tests, since we're not actually connecting
</span><span class="cx"> to LDAP.
</span><span class="cx"> """
</span><del>- return dict([(c, c.name) for c in service.fieldName.iterconstants()])
</del><ins>+ return dict([
+ (c, (c.name,))
+ for c in service.fieldName.iterconstants()
+ ])
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> def recordTypeMap(self, service):
</span><span class="lines">@@ -61,9 +66,44 @@
</span><span class="cx"> names, but we don't care for these tests, since we're not actually
</span><span class="cx"> connecting to LDAP.
</span><span class="cx"> """
</span><del>- return dict([(c, c.name) for c in service.recordType.iterconstants()])
</del><ins>+ return dict([
+ (c, (c.name,))
+ for c in service.recordType.iterconstants()
+ ])
</ins><span class="cx">
</span><span class="cx">
</span><ins>+ def _test_ldapQueryStringFromQueryStrings(self, queryStrings, expected):
+ for operand in (LDAPOperand.AND.value, LDAPOperand.OR.value):
+ compound = ldapQueryStringFromQueryStrings(operand, queryStrings)
+ self.assertEquals(compound, expected.format(operand=operand))
+
+
+ def test_ldapQueryStringFromQueryStrings_empty(self):
+ """
+ A single expression should just be returned as-is.
+ """
+ return self._test_ldapQueryStringFromQueryStrings((), u"")
+
+
+ def test_ldapQueryStringFromQueryStrings_single(self):
+ """
+ A single expression should just be returned as-is.
+ """
+ queryStrings = (u"(x=yzzy)",)
+ return self._test_ldapQueryStringFromQueryStrings(
+ queryStrings, queryStrings[0]
+ )
+
+
+ def test_ldapQueryStringFromQueryStrings_multiple(self):
+ """
+ Multiple expressions should just be combined with an operator.
+ """
+ return self._test_ldapQueryStringFromQueryStrings(
+ (u"(x=yzzy)", u"(xy=zzy)"), u"({operand}(x=yzzy)(xy=zzy))"
+ )
+
+
</ins><span class="cx"> def test_queryStringFromMatchExpression_matchTypes(self):
</span><span class="cx"> """
</span><span class="cx"> Match expressions with each match type produces the correct
</span><span class="lines">@@ -202,6 +242,110 @@
</span><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def _test_queryStringFromMatchExpression_multiAttribute(
+ self, flags, expected
+ ):
+ service = self.service()
+
+ expression = MatchExpression(
+ service.fieldName.emailAddresses, u"xyzzy",
+ flags=flags,
+ )
+
+ fieldNameToAttributeMap = {
+ service.fieldName.emailAddresses: (u"mail", u"alternateMail"),
+ }
+
+ queryString = ldapQueryStringFromMatchExpression(
+ expression, fieldNameToAttributeMap, self.recordTypeMap(service)
+ )
+
+ self.assertEquals(queryString, expected)
+
+
+ def test_queryStringFromMatchExpression_multipleAttribute(self):
+ """
+ Match expression when the queried field name maps to multiple
+ attributes.
+ """
+
+ # We want a match for either attribute.
+ expected = u"(|(mail=xyzzy)(alternateMail=xyzzy))"
+
+ return self._test_queryStringFromMatchExpression_multiAttribute(
+ MatchFlags.none, expected
+ )
+
+
+ def test_queryStringFromMatchExpression_multipleAttribute_not(self):
+ """
+ Match expression when the queried field name maps to multiple
+ attributes and the NOT flag is set.
+ """
+
+ # We want a NOT match for both attributes.
+ expected = u"(&(!mail=xyzzy)(!alternateMail=xyzzy))"
+
+ return self._test_queryStringFromMatchExpression_multiAttribute(
+ MatchFlags.NOT, expected
+ )
+
+
+ def _test_queryStringFromMatchExpression_multiRecordType(
+ self, flags, expected
+ ):
+ service = self.service()
+
+ recordTypeField = service.fieldName.recordType
+
+ expression = MatchExpression(
+ recordTypeField, service.recordType.user,
+ flags=flags,
+ )
+
+ fieldNameToAttributeMap = self.fieldNameMap(service)
+
+ recordTypeToObjectClassMap = {
+ service.recordType.user: (u"person", u"account"),
+ }
+
+ queryString = ldapQueryStringFromMatchExpression(
+ expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
+ )
+
+ self.assertEquals(
+ queryString,
+ expected.format(attr=fieldNameToAttributeMap[recordTypeField][0])
+ )
+
+
+ def test_queryStringFromMatchExpression_multipleRecordType(self):
+ """
+ Match expression when the queried field name is the record type field,
+ which maps to multiple attributes.
+ """
+
+ # We want a match for both values.
+ expected = u"(&({attr}=person)({attr}=account))"
+
+ return self._test_queryStringFromMatchExpression_multiRecordType(
+ MatchFlags.none, expected
+ )
+
+ def test_queryStringFromMatchExpression_multipleRecordType_not(self):
+ """
+ Match expression when the queried field name is the record type field,
+ which maps to multiple attributes and the NOT flag is set.
+ """
+
+ # We want a NOT match for either value.
+ expected = u"(|(!{attr}=person)(!{attr}=account))"
+
+ return self._test_queryStringFromMatchExpression_multiRecordType(
+ MatchFlags.NOT, expected
+ )
+
+
</ins><span class="cx"> def test_queryStringFromCompoundExpression_single(
</span><span class="cx"> self, queryFunction=ldapQueryStringFromCompoundExpression
</span><span class="cx"> ):
</span></span></pre>
</div>
</div>
</body>
</html>