Commit 4cf39590 authored by Randall Leeds's avatar Randall Leeds

Refactor and isolate identity, auth, and session

Introduce a new module on the frontend called `h.identity` which
abstracts the interaction between the main application and the
authentication system using the `navigator.id` API introduced by
Mozilla as part of Persona / BrowserID. In our case, the we submit
the authentication assertion as a query parameter in our token
URL. This is designed to flexibly accommodate different auth needs
by intepreting the assertion differently and using a different
identity module to return whatever type of grant is needed depending
on the authentication mechanisms in place on the back end.

On the back end:

- Introduce a dependency on a brand new library, pyramid-oauthlib, to
  make this code cleaner and more modular.
- Simplify our session by removing multiple signin code that was not
  ever fully realized; personas are no longer explicitly maintained in
  the session by application code.
- The Pyramid SessionAuthenticationPolicy is put into place as part of
  h.auth.local. A SessionGrant is configured as the default grant type
  for integration via pyramid-oauthlib. This is what interprets the
  assertion sent in our token request. For other use cases, this might
  be a real BrowserID assertion or a session or refresh token of some
  other kind. This assertion is just the CSRF token our forms have been
  returning already.
- `h#includeme` and `h#create_app` got some superficial simplification.
- `h.api#authorize` handler for annotator-store authorizations now uses
  `request.effective_principals` instead of the session, so it doesn't
  care how the user is authenticated
  - Headers are now passed through on `Store` sub-requests so that both
  the annotator auth token and the session work for store authorizations
  which gets us close to cookie-less API auth!
  - `Consumer` class is moved into `h.auth.local`, removing the SQL
  requirement for core `h` and replacing it with just the requirement to
  register an `IConsumer` implementer.

On the front end:

- Break hypothesis.js into hypothesis.js and hypothesis-auth.js
- Move session and auth modules into this auth package
- Clean up the module dependency imports
- Add an identity module to the auth package with `navigator.id` API
- Significantly refactor `AppController`
  - Use the `identity.watch()` API to listen to login/logout from the
  active identity module
  - Clean up the login/logout state management a bit
    - Resolve a promise when the API service discovery happens
    - Stop using 'session', which becomes a detail of hypothesis-auth
  - Put much less on the scope from the controller
    - `scope.initUpdater` -> `initUpdater`
    - `scope.reloadAnnotations` -> `initStore`
    - `scope.session` -> replaced by `id`
    - Sorts and views are set in the markup
    - `AuthController` no longers needs to know about `model`, `sheet`,
    `sorts` or `views`
  - Isolate the form models
    - The auth directive now creates an isolate scope so that we're not
    leaking the form models all over the place
    - Stop using the inherited `$scope.model` means prevents submitting
    `persona` as a form parameter by accident
- blocks.pt#auth-tabs becomes auth.html
  - Easy to override with `config.override_asset()` in Pyramid
  - Keeps the forms inside the isolate scope of the auth directive
- The content of the sheet moves inside blocks.pt#auth
  - Nothing outside this knows or cares anymore that the sheet has tabs
  - Places where we want to request login use `identity.request()`
  rather than having to get at the root scope. The `authorize` event
  that this broadcasts is an internal detail of the auth pacakage.
parent 37ff8175
imports = [ imports = [
'bootstrap'
'ngAnimate' 'ngAnimate'
'ngRoute' 'ngRoute'
'h.app_directives'
'h.auth' 'h.auth'
'h.controllers' 'h.controllers'
'h.directives' 'h.directives'
'h.app_directives'
'h.helpers'
'h.flash'
'h.filters' 'h.filters'
'h.session' 'h.identity'
'h.services'
'h.socket'
'h.streamsearch' 'h.streamsearch'
] ]
......
...@@ -9,6 +9,7 @@ class AuthController ...@@ -9,6 +9,7 @@ class AuthController
timeout = null timeout = null
success = -> success = ->
$scope.tab = if $scope.tab is 'forgot' then 'activate' else null
$scope.model = null $scope.model = null
$scope.$broadcast 'success' $scope.$broadcast 'success'
...@@ -36,11 +37,10 @@ class AuthController ...@@ -36,11 +37,10 @@ class AuthController
data = {} data = {}
method = '$' + form.$name method = '$' + form.$name
for own key of session angular.copy $scope.model, session
delete session[key] session.$promise = session[method] success,
angular.bind(this, failure, form)
angular.extend session, $scope.model session.$resolved = false
session[method] success, angular.bind(this, failure, form)
$scope.$on '$destroy', -> $scope.$on '$destroy', ->
if timeout if timeout
...@@ -59,36 +59,58 @@ class AuthController ...@@ -59,36 +59,58 @@ class AuthController
, 300000 , 300000
authDirective = -> authDirective = ['$timeout', ($timeout) ->
controller: 'AuthController' controller: 'AuthController'
link: (scope, elem, attrs, [form, auth]) -> link: (scope, elem, attrs, [auth, form]) ->
elem.on 'submit', (event) -> elem.on 'submit', (event) ->
scope.$apply -> scope.$apply ->
$target = angular.element event.target $target = angular.element event.target
$form = $target.controller('form') $form = $target.controller('form')
$form.responseErrorMessage = null delete $form.responseErrorMessage
for ctrl in $form.$error.response?.slice?() or [] for ctrl in $form.$error.response?.slice?() or []
ctrl.$setValidity('response', true) ctrl.$setValidity('response', true)
auth.submit($form) auth.submit($form)
scope.model = {}
scope.$on 'authorize', ->
scope.tab = 'login'
scope.$on 'error', (event) -> scope.$on 'error', (event) ->
scope[attrs.onError]?(event) scope.onError()
scope.$on 'success', (event) -> scope.$on 'success', (event) ->
scope[attrs.onSuccess]?(event) scope.onSuccess()
scope.$on 'timeout', (event) -> scope.$on 'timeout', (event) ->
scope[attrs.onTimeout]?(event) scope.onTimeout()
scope.$watch 'model', (value) -> scope.$watch 'model', (value) ->
if value is null if value is null
form.$setPristine() form.$setPristine()
require: ['form', 'auth']
scope.$watch 'tab', (name) ->
$timeout ->
elem
.find('form')
.filter(-> this.name is name)
.find('input')
.filter(-> this.type isnt 'hidden')
.first()
.focus()
require: ['auth', 'form']
restrict: 'C' restrict: 'C'
scope: true scope:
onError: '&'
onSuccess: '&'
onTimeout: '&'
session: '='
tab: '=ngModel'
templateUrl: 'auth.html'
]
angular.module('h.auth', imports) angular.module('h.auth', imports)
......
imports = [
'h.helpers'
'h.session'
]
identityFactory = [
'$rootScope', 'baseURI', 'session',
($rootScope, baseURI, session) ->
loggedInUser = undefined
onlogin = null
onlogout = null
onmatch = null
$rootScope.session = session
$rootScope.$watch 'session.$promise', (promise) ->
# Wait for any pending action to resolve.
promise.finally ->
# Get the userid and convert it to the persona format.
persona = session.userid?.replace(/^acct:/, '') or null
# Fire callbacks as appropriate.
# Consult the state matrix in the `navigator.id.watch` documentation.
# https://developer.mozilla.org/en-US/docs/Web/API/navigator.id.watch
if loggedInUser is null
if persona
loggedInUser = persona
onlogin?(session.csrf_token)
else
onmatch?()
else if loggedInUser
if persona
if loggedInUser is persona
onmatch?()
else
loggedInUser = persona
onlogin?(session.csrf_token)
else
loggedInUser = null
onlogout?()
else
if persona
loggedInUser = persona
onlogin?(session.csrf_token)
else
loggedInUser = null
onlogout?()
logout: ->
# Clear the session but preserve its identity and give it a new promise.
$promise = session.$logout()
$resolved = false
angular.copy({$promise, $resolved}, session)
request: ->
$rootScope.$broadcast 'authorize'
watch: (options) ->
{loggedInUser, onlogin, onlogout, onmatch} = options
]
angular.module('h.identity', imports)
.factory('identity', identityFactory)
...@@ -73,14 +73,6 @@ class SessionProvider ...@@ -73,14 +73,6 @@ class SessionProvider
model.errors = data.errors model.errors = data.errors
model.reason = data.reason model.reason = data.reason
# bw compat
if angular.isObject(data.persona)
persona = data.persona
data.persona = "acct:#{persona.username}@#{persona.provider}"
data.personas = for persona in data.personas
"acct:#{persona.username}@#{persona.provider}"
# end bw compat
# Fire flash messages. # Fire flash messages.
for q, msgs of data.flash for q, msgs of data.flash
flash q, msgs flash q, msgs
......
imports = [ imports = [
'bootstrap' 'bootstrap'
'h.flash'
'h.helpers' 'h.helpers'
'h.identity'
'h.services'
'h.socket' 'h.socket'
'h.searchfilters' 'h.searchfilters'
] ]
class App class App
scope:
frame:
visible: false
ongoingHighlightSwitch: false
search: {}
sheet: {}
sorts: [
'Newest'
'Oldest'
'Location'
]
views: [
'Screen'
'Document'
'Comments'
]
this.$inject = [ this.$inject = [
'$element', '$location', '$q', '$rootScope', '$route', '$scope', '$timeout', '$element', '$location', '$q', '$rootScope', '$route', '$scope', '$timeout',
'annotator', 'flash', 'session', 'socket', 'streamfilter', 'viewFilter' 'annotator', 'flash', 'identity', 'queryparser', 'socket',
'streamfilter', 'viewFilter'
] ]
constructor: ( constructor: (
$element, $location, $q, $rootScope, $route, $scope, $timeout $element, $location, $q, $rootScope, $route, $scope, $timeout
annotator, flash, session, socket, streamfilter, viewFilter annotator, flash, identity, queryparser, socket,
streamfilter, viewFilter
) -> ) ->
{plugins, host, providers} = annotator {plugins, host, providers} = annotator
_reset = => # Verified user id.
delete annotator.ongoing_edit # Undefined means we don't track the session, but the identity module will
base = angular.copy @scope # tell us the state of the session. A null value means that the session
angular.extend $scope, base, # has been checked and it was found that there is no user logged in.
frame: $scope.frame or @scope.frame loggedInUser = undefined
socialView: annotator.socialView
ongoingHighlightSwitch: false # Resolved once the API service has been discovered.
search: storeReady = $q.defer()
query: $location.search()['q']
session: session configureIdentity = (persona) ->
# Store the argument as the claimed user id.
_reset() claimedUser = persona
annotator.subscribe 'serviceDiscovery', (options) -> # Convert it to the format used by persona.
annotator.options.Store ?= {} if claimedUser then claimedUser = claimedUser.replace(/^acct:/, '')
angular.extend annotator.options.Store, options
if claimedUser is loggedInUser
if loggedInUser is undefined
# This is the first execution.
# Configure the identity callbacks and the initial user id claim.
identity.watch
loggedInUser: claimedUser
onlogin: (assertion) ->
onlogin(assertion)
onlogout: ->
onlogout()
else if annotator.discardDrafts()
if claimedUser
identity.request()
else
identity.logout()
session.$promise.then (data) -> onlogin = (assertion) ->
unless data.personas?.length # Delete any old Auth plugin.
$scope.initUpdater() delete plugins.Auth
$scope.reloadAnnotations()
$scope.$watch 'session.personas', (newValue, oldValue) => # Configure the Auth plugin with the issued assertion as refresh token.
if newValue?.length annotator.addPlugin 'Auth',
unless $scope.session.persona and $scope.session.persona in newValue tokenUrl: "#{baseURI}api/token?assertion=#{assertion}"
$scope.session.persona = newValue[0]
else
$scope.session.persona = null
$scope.$watch 'session.persona', (newValue, oldValue) => # Set the user from the token.
unless annotator.discardDrafts() plugins.Auth.withToken (token) ->
$scope.session.persona = oldValue plugins.Permissions._setAuthFromToken(token)
return loggedInUser = plugins.Permissions.user.replace /^acct:/, ''
reset()
onlogout = ->
plugins.Auth?.element.removeData('annotator:headers') plugins.Auth?.element.removeData('annotator:headers')
delete plugins.Auth delete plugins.Auth
plugins.Permissions?.setUser(null) plugins.Permissions.setUser(null)
# XXX: Temporary workaround until Annotator v2.0 or v1.2.10 # XXX: Temporary workaround until Annotator v2.0 or v1.2.10
plugins.Permissions?.options.permissions = plugins.Permissions.options.permissions =
read: [] read: []
update: [] update: []
delete: [] delete: []
admin: [] admin: []
if newValue? loggedInUser = null
annotator.addPlugin 'Auth', tokenUrl: "/api/token?persona=#{newValue}" reset()
plugins.Auth.withToken (token) =>
plugins.Permissions._setAuthFromToken token
reset = ->
# Do not rely on the identity service to invoke callbacks within an
# angular digest cycle.
$scope.$evalAsync ->
if annotator.ongoing_edit if annotator.ongoing_edit
$timeout ->
annotator.clickAdder() annotator.clickAdder()
, 1000
if $scope.ongoingHighlightSwitch if $scope.ongoingHighlightSwitch
$scope.ongoingHighlightSwitch = false $scope.ongoingHighlightSwitch = false
annotator.setTool 'highlight' annotator.setTool 'highlight'
else else
$scope.reloadAnnotations()
$scope.initUpdater()
else if oldValue?
session.$logout =>
$scope.$broadcast 'reset'
if annotator.tool isnt 'comment'
annotator.setTool 'comment' annotator.setTool 'comment'
else
$scope.reloadAnnotations() # Convert the verified user id to the format used by the API.
$scope.initUpdater() persona = loggedInUser
if persona then persona = "acct:#{persona}"
# Ensure it is synchronized on the scope.
# Without this, failed identity changes will remain on the scope.
$scope.persona = persona
# Reload services
storeReady.promise.then ->
initStore()
initUpdater()
annotator.subscribe 'serviceDiscovery', (options) ->
annotator.options.Store ?= {}
angular.extend annotator.options.Store, options
storeReady.resolve()
$scope.$watch 'persona', configureIdentity
$scope.$watch 'socialView.name', (newValue, oldValue) -> $scope.$watch 'socialView.name', (newValue, oldValue) ->
return if newValue is oldValue return if newValue is oldValue
console.log "Social View changed to '" + newValue + "'. Reloading annotations."
$scope.reloadAnnotations() if $scope.persona
initStore()
else if newValue is 'single-player'
identity.request()
$scope.$watch 'frame.visible', (newValue, oldValue) -> $scope.$watch 'frame.visible', (newValue, oldValue) ->
routeName = $location.path().replace /^\//, '' routeName = $location.path().replace /^\//, ''
...@@ -121,20 +135,6 @@ class App ...@@ -121,20 +135,6 @@ class App
for p in annotator.providers for p in annotator.providers
p.channel.notify method: 'setActiveHighlights' p.channel.notify method: 'setActiveHighlights'
$scope.$watch 'sheet.show', (visible) ->
$scope.sheet.tab = if visible then 'login' else null
$scope.$watch 'sheet.tab', (tab) ->
$timeout ->
$element
.find('form')
.filter(-> this.name is tab)
.find('input')
.filter(-> this.type isnt 'hidden')
.first()
.focus()
, 10
$scope.$watch 'store.entities', (entities, oldEntities) -> $scope.$watch 'store.entities', (entities, oldEntities) ->
return if entities is oldEntities return if entities is oldEntities
...@@ -232,15 +232,7 @@ class App ...@@ -232,15 +232,7 @@ class App
$scope.updater.then (sock) -> $scope.updater.then (sock) ->
sock.send(JSON.stringify(sockmsg)) sock.send(JSON.stringify(sockmsg))
$scope.search.clear = -> initStore = ->
$location.search('q', null)
$scope.search.update = (query) ->
unless angular.equals $location.search()['q'], query
if annotator.discardDrafts()
$location.search('q', query or null)
$scope.reloadAnnotations = ->
Store = plugins.Store Store = plugins.Store
delete plugins.Store delete plugins.Store
...@@ -323,15 +315,29 @@ class App ...@@ -323,15 +315,29 @@ class App
cleanup (a for a in annotations when a.thread) cleanup (a for a in annotations when a.thread)
annotator.subscribe 'annotationsLoaded', cleanup annotator.subscribe 'annotationsLoaded', cleanup
$scope.authSuccess = ->
$scope.sheet.show = false
$scope.authTimeout = -> $scope.authTimeout = ->
delete annotator.ongoing_edit
$scope.ongoingHighlightSwitch = false
flash 'info', flash 'info',
'For your security, the forms have been reset due to inactivity.' 'For your security, the forms have been reset due to inactivity.'
_reset()
$scope.initUpdater = (failureCount=0) -> $scope.frame = visible: false
$scope.id = identity
$scope.model = persona: undefined
$scope.search =
clear: ->
$location.search('q', null)
update: (query) ->
unless angular.equals $location.search()['q'], query
if annotator.discardDrafts()
$location.search('q', query or null)
$scope.socialView = annotator.socialView
initUpdater = (failureCount=0) ->
_dfdSock = $q.defer() _dfdSock = $q.defer()
_sock = socket() _sock = socket()
...@@ -350,7 +356,7 @@ class App ...@@ -350,7 +356,7 @@ class App
failureCount = Math.min(10, ++failureCount) failureCount = Math.min(10, ++failureCount)
slots = Math.random() * (Math.pow(2, failureCount) - 1) slots = Math.random() * (Math.pow(2, failureCount) - 1)
$timeout -> $timeout ->
_retry = $scope.initUpdater(failureCount) _retry = initUpdater(failureCount)
_dfdSock?.resolve(_retry) _dfdSock?.resolve(_retry)
, slots * 500 , slots * 500
...@@ -363,7 +369,7 @@ class App ...@@ -363,7 +369,7 @@ class App
unless data instanceof Array then data = [data] unless data instanceof Array then data = [data]
p = $scope.session.persona p = $scope.persona
user = if p? then "acct:" + p.username + "@" + p.provider else '' user = if p? then "acct:" + p.username + "@" + p.provider else ''
unless data instanceof Array then data = [data] unless data instanceof Array then data = [data]
......
...@@ -235,13 +235,6 @@ thread = ['$rootScope', '$window', ($rootScope, $window) -> ...@@ -235,13 +235,6 @@ thread = ['$rootScope', '$window', ($rootScope, $window) ->
] ]
userPicker = ->
restrict: 'ACE'
scope:
model: '=userPickerModel'
options: '=userPickerOptions'
templateUrl: 'userPicker.html'
repeatAnim = -> repeatAnim = ->
restrict: 'A' restrict: 'A'
scope: scope:
...@@ -446,7 +439,6 @@ angular.module('h.directives', ['ngSanitize']) ...@@ -446,7 +439,6 @@ angular.module('h.directives', ['ngSanitize'])
.directive('tags', tags) .directive('tags', tags)
.directive('thread', thread) .directive('thread', thread)
.directive('username', username) .directive('username', username)
.directive('userPicker', userPicker)
.directive('repeatAnim', repeatAnim) .directive('repeatAnim', repeatAnim)
.directive('simpleSearch', simpleSearch) .directive('simpleSearch', simpleSearch)
.directive('whenscrolled', whenscrolled) .directive('whenscrolled', whenscrolled)
...@@ -435,13 +435,13 @@ class Hypothesis extends Annotator ...@@ -435,13 +435,13 @@ class Hypothesis extends Annotator
showEditor: (annotation) => showEditor: (annotation) =>
this.show() this.show()
@element.injector().invoke [ @element.injector().invoke [
'$location', '$rootScope', 'drafts' '$location', '$rootScope', 'drafts', 'identity',
($location, $rootScope, drafts) => ($location, $rootScope, drafts, identity) =>
@ongoing_edit = annotation @ongoing_edit = annotation
unless this.plugins.Auth? and this.plugins.Auth.haveValidToken() unless this.plugins.Auth? and this.plugins.Auth.haveValidToken()
$rootScope.$apply -> $rootScope.$apply ->
$rootScope.$broadcast 'showAuth', true identity.request()
for p in @providers for p in @providers
p.channel.notify method: 'onEditorHide' p.channel.notify method: 'onEditorHide'
return return
...@@ -566,7 +566,7 @@ class Hypothesis extends Annotator ...@@ -566,7 +566,7 @@ class Hypothesis extends Annotator
scope = @element.scope() scope = @element.scope()
# If we are not logged in, start the auth process # If we are not logged in, start the auth process
scope.ongoingHighlightSwitch = true scope.ongoingHighlightSwitch = true
scope.sheet.collapsed = false @element.injector().get('identity').request()
this.show() this.show()
return return
......
...@@ -5,18 +5,17 @@ imports = [ ...@@ -5,18 +5,17 @@ imports = [
'h.filters' 'h.filters'
'h.flash' 'h.flash'
'h.helpers' 'h.helpers'
'h.session'
'h.searchfilters' 'h.searchfilters'
] ]
class StreamSearch class StreamSearch
this.inject = [ this.inject = [
'$scope', '$rootScope', '$scope', '$rootScope',
'queryparser', 'session', 'searchfilter', 'streamfilter' 'queryparser', 'searchfilter', 'streamfilter'
] ]
constructor: ( constructor: (
$scope, $rootScope, $scope, $rootScope,
queryparser, session, searchfilter, streamfilter queryparser, searchfilter, streamfilter
) -> ) ->
# Initialize the base filter # Initialize the base filter
streamfilter streamfilter
......
...@@ -53,9 +53,11 @@ module.exports = function(config) { ...@@ -53,9 +53,11 @@ module.exports = function(config) {
'h/static/scripts/vendor/jquery.ui.effect-highlight.js', 'h/static/scripts/vendor/jquery.ui.effect-highlight.js',
'h/static/scripts/vendor/tag-it.js', 'h/static/scripts/vendor/tag-it.js',
'h/static/scripts/vendor/uuid.js', 'h/static/scripts/vendor/uuid.js',
'h/static/scripts/hypothesis-auth.js',
'h/static/scripts/hypothesis.js', 'h/static/scripts/hypothesis.js',
'h/static/scripts/vendor/sinon.js', 'h/static/scripts/vendor/sinon.js',
'h/static/scripts/vendor/chai.js', 'h/static/scripts/vendor/chai.js',
'h/templates/*.html',
'tests/js/**/*-test.coffee' 'tests/js/**/*-test.coffee'
], ],
...@@ -64,11 +66,16 @@ module.exports = function(config) { ...@@ -64,11 +66,16 @@ module.exports = function(config) {
exclude: [ exclude: [
], ],
// strip templates of leading path
ngHtml2JsPreprocessor: {
stripPrefix: 'h/templates/'
},
// preprocess matching files before serving them to the browser // preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: { preprocessors: {
'**/*.coffee': ['coffee'] '**/*.coffee': ['coffee'],
'**/*.html': ['html2js']
}, },
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
"karma-cli": "0.0.4", "karma-cli": "0.0.4",
"karma-coffee-preprocessor": "^0.2.1", "karma-coffee-preprocessor": "^0.2.1",
"karma-mocha": "^0.1.4", "karma-mocha": "^0.1.4",
"karma-ng-html2js-preprocessor": "^0.1.0",
"karma-phantomjs-launcher": "^0.1.4", "karma-phantomjs-launcher": "^0.1.4",
"mocha": "^1.20.1", "mocha": "^1.20.1",
"phantomjs": "1.9.7-10" "phantomjs": "1.9.7-10"
......
...@@ -14,6 +14,7 @@ class MockSession ...@@ -14,6 +14,7 @@ class MockSession
describe 'h.auth', -> describe 'h.auth', ->
beforeEach module('h.auth') beforeEach module('h.auth')
beforeEach module('auth.html')
beforeEach module ($provide) -> beforeEach module ($provide) ->
$provide.value '$timeout', sinon.spy() $provide.value '$timeout', sinon.spy()
...@@ -97,39 +98,41 @@ describe 'h.auth', -> ...@@ -97,39 +98,41 @@ describe 'h.auth', ->
elem = angular.element( elem = angular.element(
''' '''
<div class="auth" ng-form="form" <div class="auth" ng-form="form"
on-error="stub" on-success="stub" on-timeout="stub"> on-error="stub()" on-success="stub()" on-timeout="stub()">
<form name="login">
<input type="text" name="username" ng-model="username"></input>
</form>
</div> </div>
''' '''
) )
session = _session_ session = _session_
$rootScope = _$rootScope_ $rootScope = _$rootScope_
$scope = $compile(elem)($rootScope).scope()
$scope.$digest() $compile(elem)($rootScope)
$rootScope.$digest()
$scope = elem.isolateScope()
it 'should reset response errors before submit', -> it 'should reset response errors before submit', ->
$scope.form.login.responseErrorMessage = 'test' $scope.login.username.$setViewValue('test')
$scope.form.login.username.$setValidity('response', false) $scope.login.password.$setViewValue('1234')
assert.isFalse $scope.form.login.$valid $scope.login.responseErrorMessage = 'test'
$scope.login.username.$setValidity('response', false)
assert.isFalse $scope.login.$valid
elem.find('input').trigger('submit') elem.find('input').trigger('submit')
assert.isTrue $scope.form.login.$valid assert.isTrue $scope.login.$valid
assert.isNull $scope.form.login.responseErrorMessage assert.isUndefined $scope.login.responseErrorMessage
it 'should reset to pristine state when the model is reset', -> it 'should reset to pristine state when the model is reset', ->
$scope.form.$setDirty() $rootScope.form.$setDirty()
$scope.$digest() $rootScope.$digest()
assert.isFalse $scope.form.$pristine assert.isFalse $rootScope.form.$pristine
$scope.model = null $scope.model = null
$scope.$digest() $scope.$digest()
assert.isTrue $scope.form.$pristine assert.isTrue $rootScope.form.$pristine
it 'should invoke handlers set by attributes', -> it 'should invoke handlers set by attributes', ->
$scope.stub = sinon.stub() $rootScope.stub = sinon.stub()
for event in ['error', 'success', 'timeout'] for event in ['error', 'success', 'timeout']
$scope.stub.reset() $rootScope.stub.reset()
$scope.$broadcast(event) $scope.$broadcast(event)
assert.called $scope.stub assert.called $rootScope.stub
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment