Merge branch 'master' into release

This commit is contained in:
Dan Brown 2016-02-08 20:45:48 +00:00
commit b97e792c5f
33 changed files with 345 additions and 107 deletions

1
.gitignore vendored
View File

@ -7,7 +7,6 @@ Homestead.yaml
/public/plugins
/public/css/*.map
/public/js/*.map
/public/uploads
/public/bower
/storage/images
_ide_helper.php

View File

@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class ConfirmationEmailException extends NotifyException
{
}
class ConfirmationEmailException extends NotifyException {}

View File

@ -5,6 +5,7 @@ namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;
@ -44,11 +45,20 @@ class Handler extends ExceptionHandler
*/
public function render($request, Exception $e)
{
if($e instanceof NotifyException) {
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($e instanceof NotifyException) {
\Session::flash('error', $e->message);
return response()->redirectTo($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if (($e instanceof PrettyException || $e->getPrevious() instanceof PrettyException) && !config('app.debug')) {
$message = ($e instanceof PrettyException) ? $e->getMessage() : $e->getPrevious()->getMessage();
return response()->view('errors/500', ['message' => $message], 500);
}
return parent::render($request, $e);
}
}

View File

@ -1,6 +1,3 @@
<?php namespace BookStack\Exceptions;
use Exception;
class ImageUploadException extends Exception {}
class ImageUploadException extends PrettyException {}

View File

@ -1,9 +1,3 @@
<?php namespace BookStack\Exceptions;
use Exception;
class LdapException extends Exception
{
}
class LdapException extends PrettyException {}

View File

@ -0,0 +1,5 @@
<?php namespace BookStack\Exceptions;
use Exception;
class PrettyException extends Exception {}

View File

@ -1,6 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialDriverNotConfigured extends \Exception
{
}
class SocialDriverNotConfigured extends PrettyException {}

View File

@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class SocialSignInException extends NotifyException
{
}
class SocialSignInException extends NotifyException {}

View File

@ -1,7 +1,4 @@
<?php namespace BookStack\Exceptions;
class UserRegistrationException extends NotifyException
{
}
class UserRegistrationException extends NotifyException {}

View File

@ -130,8 +130,8 @@ class UserController extends Controller
});
$this->validate($request, [
'name' => 'required',
'email' => 'required|email|unique:users,email,' . $id,
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'role' => 'exists:roles,id'

View File

@ -4,6 +4,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Image;
use BookStack\User;
use Exception;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
@ -123,6 +124,8 @@ class ImageService
* @param int $height
* @param bool $keepRatio
* @return string
* @throws Exception
* @throws ImageUploadException
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
@ -139,8 +142,16 @@ class ImageService
return $this->getPublicUrl($thumbFilePath);
}
// Otherwise create the thumbnail
try {
$thumb = $this->imageTool->make($storage->get($image->path));
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException('The server cannot create thumbnails. Please check you have the GD PHP extension installed.');
} else {
throw $e;
}
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$constraint->aspectRatio();

View File

@ -46,7 +46,7 @@ class LdapService
$user = $users[0];
return [
'uid' => $user['uid'][0],
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null

2
public/uploads/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -17,19 +17,13 @@ A platform to create documentation/wiki content. General information about BookS
## Requirements
BookStack has similar requirements to Laravel. On top of those are some front-end build tools which are only required when developing.
BookStack has similar requirements to Laravel:
* PHP >= 5.5.9, Will need to be usable from the command line.
* OpenSSL PHP Extension
* PDO PHP Extension
* MBstring PHP Extension
* Tokenizer PHP Extension
* PHP Extensions: `OpenSSL`, `PDO`, `MBstring`, `Tokenizer`, `GD`
* MySQL >= 5.6
* Git (Not strictly required but helps manage updates)
* [Composer](https://getcomposer.org/)
* [Node.js](https://nodejs.org/en/) **Development Only**
* [Gulp](http://gulpjs.com/) **Development Only**
## Installation
@ -144,7 +138,14 @@ A user in BookStack will be linked to a LDAP user via a 'uid'. If a LDAP user ui
You may find that you cannot log in with your initial Admin account after changing the `AUTH_METHOD` to `ldap`. To get around this set the `AUTH_METHOD` to `standard`, login with your admin account then change it back to `ldap`. You get then edit your profile and add your LDAP uid under the 'External Authentication ID' field. You will then be able to login in with that ID.
## Testing
## Development & Testing
All development on BookStack is currently done on the master branch. When it's time for a release the master branch is merged into release with built & minified CSS & JS then tagged at it's version. Here are the current development requirements:
* [Node.js](https://nodejs.org/en/) **Development Only**
* [Gulp](http://gulpjs.com/) **Development Only**
SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp.
BookStack has many integration tests that use Laravel's built-in testing capabilities which makes use of PHPUnit. To use you will need PHPUnit installed and accessible via command line. There is a `mysql_testing` database defined within the app config which is what is used by PHPUnit. This database is set with the following database name, user name and password defined as `bookstack-test`. You will have to create that database and credentials before testing.

View File

@ -1,6 +1,6 @@
"use strict";
module.exports = function (ngApp) {
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
@ -17,21 +17,40 @@ module.exports = function (ngApp) {
var dataLoaded = false;
var callback = false;
/**
* Simple returns the appropriate upload url depending on the image type set.
* @returns {string}
*/
$scope.getUploadUrl = function () {
return '/images/' + $scope.imageType + '/upload';
};
/**
* Runs on image upload, Adds an image to local list of images
* and shows a success message to the user.
* @param file
* @param data
*/
$scope.uploadSuccess = function (file, data) {
$scope.$apply(() => {
$scope.images.unshift(data);
});
events.emit('success', 'Image uploaded');
};
/**
* Runs the callback and hides the image manager.
* @param returnData
*/
function callbackAndHide(returnData) {
if (callback) callback(returnData);
$scope.showing = false;
}
/**
* Image select action. Checks if a double-click was fired.
* @param image
*/
$scope.imageSelect = function (image) {
var dblClickTime = 300;
var currentTime = Date.now();
@ -48,10 +67,19 @@ module.exports = function (ngApp) {
previousClickTime = currentTime;
};
/**
* Action that runs when the 'Select image' button is clicked.
* Runs the callback and hides the image manager.
*/
$scope.selectButtonClick = function () {
callbackAndHide($scope.selectedImage);
};
/**
* Show the image manager.
* Takes a callback to execute later on.
* @param doneCallback
*/
function show(doneCallback) {
callback = doneCallback;
$scope.showing = true;
@ -62,6 +90,8 @@ module.exports = function (ngApp) {
}
}
// Connects up the image manger so it can be used externally
// such as from TinyMCE.
imageManagerService.show = show;
imageManagerService.showExternal = function (doneCallback) {
$scope.$apply(() => {
@ -70,10 +100,16 @@ module.exports = function (ngApp) {
};
window.ImageManager = imageManagerService;
/**
* Hide the image manager
*/
$scope.hide = function () {
$scope.showing = false;
};
/**
* Fetch the list image data from the server.
*/
function fetchData() {
var url = '/images/' + $scope.imageType + '/all/' + page;
$http.get(url).then((response) => {
@ -82,28 +118,33 @@ module.exports = function (ngApp) {
page++;
});
}
$scope.fetchData = fetchData;
/**
* Save the details of an image.
* @param event
*/
$scope.saveImageDetails = function (event) {
event.preventDefault();
var url = '/images/update/' + $scope.selectedImage.id;
$http.put(url, this.selectedImage).then((response) => {
$scope.imageUpdateSuccess = true;
$timeout(() => {
$scope.imageUpdateSuccess = false;
}, 3000);
events.emit('success', 'Image details updated');
}, (response) => {
var errors = response.data;
var message = '';
Object.keys(errors).forEach((key) => {
message += errors[key].join('\n');
});
$scope.imageUpdateFailure = message;
$timeout(() => {
$scope.imageUpdateFailure = false;
}, 5000);
events.emit('error', message);
});
};
/**
* Delete an image from system and notify of success.
* Checks if it should force delete when an image
* has dependant pages.
* @param event
*/
$scope.deleteImage = function (event) {
event.preventDefault();
var force = $scope.dependantPages !== false;
@ -112,10 +153,7 @@ module.exports = function (ngApp) {
$http.delete(url).then((response) => {
$scope.images.splice($scope.images.indexOf($scope.selectedImage), 1);
$scope.selectedImage = false;
$scope.imageDeleteSuccess = true;
$timeout(() => {
$scope.imageDeleteSuccess = false;
}, 3000);
events.emit('success', 'Image successfully deleted');
}, (response) => {
// Pages failure
if (response.status === 400) {
@ -124,6 +162,15 @@ module.exports = function (ngApp) {
});
};
/**
* Simple date creator used to properly format dates.
* @param stringDate
* @returns {Date}
*/
$scope.getDate = function(stringDate) {
return new Date(stringDate);
};
}]);

View File

@ -5,7 +5,7 @@ var toggleSwitchTemplate = require('./components/toggle-switch.html');
var imagePickerTemplate = require('./components/image-picker.html');
var dropZoneTemplate = require('./components/drop-zone.html');
module.exports = function (ngApp) {
module.exports = function (ngApp, events) {
/**
* Toggle Switches

View File

@ -1,4 +1,4 @@
"use strict";
// AngularJS - Create application and load components
var angular = require('angular');
@ -7,9 +7,31 @@ var ngAnimate = require('angular-animate');
var ngSanitize = require('angular-sanitize');
var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
var services = require('./services')(ngApp);
var directives = require('./directives')(ngApp);
var controllers = require('./controllers')(ngApp);
// Global Event System
var Events = {
listeners: {},
emit: function (eventName, eventData) {
if (typeof this.listeners[eventName] === 'undefined') return this;
var eventsToStart = this.listeners[eventName];
for (let i = 0; i < eventsToStart.length; i++) {
var event = eventsToStart[i];
event(eventData);
}
return this;
},
listen: function (eventName, callback) {
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
this.listeners[eventName].push(callback);
return this;
}
};
window.Events = Events;
var services = require('./services')(ngApp, Events);
var directives = require('./directives')(ngApp, Events);
var controllers = require('./controllers')(ngApp, Events);
//Global jQuery Config & Extensions
@ -32,8 +54,25 @@ $.expr[":"].contains = $.expr.createPseudo(function (arg) {
// Global jQuery Elements
$(function () {
var notifications = $('.notification');
var successNotification = notifications.filter('.pos');
var errorNotification = notifications.filter('.neg');
// Notification Events
window.Events.listen('success', function (text) {
successNotification.hide();
successNotification.find('span').text(text);
setTimeout(() => {
successNotification.show();
}, 1);
});
window.Events.listen('error', function (text) {
errorNotification.find('span').text(text);
errorNotification.show();
});
// Notification hiding
$('.notification').click(function () {
notifications.click(function () {
$(this).fadeOut(100);
});
@ -44,6 +83,29 @@ $(function () {
$(this).closest('.chapter').find('.inset-list').slideToggle(180);
});
// Back to top button
$('#back-to-top').click(function() {
$('#header').smoothScrollTo();
});
var scrollTopShowing = false;
var scrollTop = document.getElementById('back-to-top');
var scrollTopBreakpoint = 1200;
window.addEventListener('scroll', function() {
if (!scrollTopShowing && document.body.scrollTop > scrollTopBreakpoint) {
scrollTop.style.display = 'block';
scrollTopShowing = true;
setTimeout(() => {
scrollTop.style.opacity = 1;
}, 1);
} else if (scrollTopShowing && document.body.scrollTop < scrollTopBreakpoint) {
scrollTop.style.opacity = 0;
scrollTopShowing = false;
setTimeout(() => {
scrollTop.style.display = 'none';
}, 500);
}
});
});

View File

@ -8,7 +8,6 @@ module.exports = {
statusbar: false,
menubar: false,
paste_data_images: false,
//height: 700,
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
@ -31,7 +30,7 @@ module.exports = {
alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'},
},
file_browser_callback: function (field_name, url, type, win) {
ImageManager.show(function (image) {
window.ImageManager.showExternal(function (image) {
win.document.getElementById(field_name).value = image.url;
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
@ -40,6 +39,10 @@ module.exports = {
} else {
win.document.getElementById(field_name).fireEvent("onchange");
}
var html = '<a href="' + image.url + '" target="_blank">';
html += '<img src="' + image.thumbs.display + '" alt="' + image.name + '">';
html += '</a>';
win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
});
},
paste_preprocess: function (plugin, args) {

View File

@ -1,6 +1,6 @@
"use strict";
module.exports = function(ngApp) {
module.exports = function(ngApp, events) {
ngApp.factory('imageManagerService', function() {
return {

View File

@ -21,7 +21,6 @@
border-radius: 4px;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.3);
overflow: hidden;
max-width: 1340px;
position: fixed;
top: 0;
bottom: 0;
@ -44,18 +43,49 @@
right: 0;
}
.image-manager-list img {
.image-manager-list .image {
display: block;
position: relative;
border-radius: 0;
float: left;
margin: 0;
cursor: pointer;
width: (100%/6);
height: auto;
border: 1px solid #FFF;
border: 1px solid #DDD;
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
transition: all cubic-bezier(.4, 0, 1, 1) 160ms;
overflow: hidden;
&.selected {
transform: scale3d(0.92, 0.92, 0.92);
border: 1px solid #444;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
}
img {
width: 100%;
max-width: 100%;
display: block;
}
.image-meta {
position: absolute;
width: 100%;
bottom: 0;
left: 0;
color: #EEE;
background-color: rgba(0, 0, 0, 0.4);
font-size: 10px;
padding: 3px 4px;
span {
display: block;
}
}
@include smaller-than($xl) {
width: (100%/4);
}
@include smaller-than($m) {
.image-meta {
display: none;
}
}
}

View File

@ -105,8 +105,8 @@
}
.book-tree .sidebar-page-list {
list-style: none;
margin: 0;
margin-top: $-xs;
margin: $-xs 0 0;
padding-left: 0;
border-left: 5px solid $color-book;
li a {
display: block;

View File

@ -223,13 +223,13 @@ span.highlight {
* Lists
*/
ul {
list-style: disc;
margin-left: $-m*1.5;
padding-left: $-m * 1.5;
list-style: disc inside;
}
ol {
list-style: decimal;
margin-left: $-m*1.5;
list-style: decimal inside;
padding-left: $-m * 1.5;
}
/*

View File

@ -10,3 +10,8 @@
@import "header";
@import "lists";
@import "pages";
table {
border-spacing: 0;
border-collapse: collapse;
}

View File

@ -127,3 +127,41 @@ $loadingSize: 10px;
padding-right: $-s;
}
}
// Back to top link
$btt-size: 40px;
#back-to-top {
background-color: rgba($primary, 0.4);
position: fixed;
bottom: $-m;
right: $-m;
padding: $-xs $-s;
cursor: pointer;
color: #FFF;
width: $btt-size;
height: $btt-size;
border-radius: $btt-size;
transition: all ease-in-out 180ms;
opacity: 0;
z-index: 999;
&:hover {
width: $btt-size*3.4;
background-color: rgba($primary, 1);
span {
display: inline-block;
}
}
.inner {
width: $btt-size*3.4;
}
i {
margin: 0;
font-size: 28px;
padding: 0 $-s 0 0;
}
span {
line-height: 12px;
position: relative;
top: -5px;
}
}

View File

@ -77,6 +77,11 @@
@yield('content')
</section>
<div id="back-to-top">
<div class="inner">
<i class="zmdi zmdi-chevron-up"></i> <span>Back to top</span>
</div>
</div>
@yield('bottom')
<script src="{{ versioned_asset('js/common.js') }}"></script>
@yield('scripts')

View File

@ -0,0 +1,10 @@
@extends('base')
@section('content')
<div class="container">
<h1 class="text-muted">An Error Occurred</h1>
<p>{{ $message }}</p>
</div>
@stop

View File

@ -7,12 +7,12 @@
<div class="faded-small">
<div class="container">
<div class="row">
<div class="col-md-4 faded">
<div class="col-sm-4 faded">
<div class="action-buttons text-left">
<a onclick="$('body>header').slideToggle();" class="text-button text-primary"><i class="zmdi zmdi-swap-vertical"></i>Toggle Header</a>
</div>
</div>
<div class="col-md-8 faded">
<div class="col-sm-8 faded">
<div class="action-buttons">
<a href="{{ back()->getTargetUrl() }}" class="text-button text-primary"><i class="zmdi zmdi-close"></i>Cancel</a>
<button type="submit" id="save-button" class="text-button text-pos"><i class="zmdi zmdi-floppy"></i>Save Page</button>

View File

@ -20,5 +20,11 @@
table td {
width: auto !important;
}
.page-content img.align-left, .page-content img.align-right {
float: none !important;
clear: both;
display: block;
}
</style>
@stop

View File

@ -22,9 +22,9 @@
<span dropdown class="dropdown-container">
<div dropdown-toggle class="text-button text-primary"><i class="zmdi zmdi-open-in-new"></i>Export</div>
<ul class="wide">
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted pull-right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted pull-right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted pull-right">.txt</span></a></li>
<li><a href="{{$page->getUrl() . '/export/html'}}" target="_blank">Contained Web File <span class="text-muted float right">.html</span></a></li>
<li><a href="{{$page->getUrl() . '/export/pdf'}}" target="_blank">PDF File <span class="text-muted float right">.pdf</span></a></li>
<li><a href="{{$page->getUrl() . '/export/plaintext'}}" target="_blank">Plain Text File <span class="text-muted float right">.txt</span></a></li>
</ul>
</span>
@if($currentUser->can('page-update'))

View File

@ -5,11 +5,14 @@
<div class="image-manager-content">
<div class="image-manager-list">
<div ng-repeat="image in images">
<img class="anim fadeIn"
ng-class="{selected: (image==selectedImage)}"
ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}"
ng-click="imageSelect(image)"
ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}">
<div class="image anim fadeIn" ng-style="{animationDelay: ($index > 26) ? '160ms' : ($index * 25) + 'ms'}"
ng-class="{selected: (image==selectedImage)}" ng-click="imageSelect(image)">
<img ng-src="@{{image.thumbs.gallery}}" ng-attr-alt="@{{image.title}}" ng-attr-title="@{{image.name}}">
<div class="image-meta">
<span class="name" ng-bind="image.name"></span>
<span class="date">Uploaded @{{ getDate(image.created_at) | date:'mediumDate' }}</span>
</div>
</div>
</div>
<div class="load-more" ng-show="hasMore" ng-click="fetchData()">Load More</div>
</div>
@ -19,18 +22,20 @@
<div class="image-manager-sidebar">
<h2>Images</h2>
<hr class="even">
<drop-zone upload-url="@{{getUploadUrl()}}" event-success="uploadSuccess"></drop-zone>
<div class="image-manager-details anim fadeIn" ng-show="selectedImage">
<hr class="even">
<form ng-submit="saveImageDetails($event)">
<div>
<a ng-href="@{{selectedImage.url}}" target="_blank" style="display: block;">
<img ng-src="@{{selectedImage.thumbs.gallery}}" ng-attr-alt="@{{selectedImage.title}}" ng-attr-title="@{{selectedImage.name}}">
</a>
</div>
<div class="form-group">
<label for="name">Image Name</label>
<input type="text" id="name" name="name" ng-model="selectedImage.name">
<p class="text-pos text-small" ng-show="imageUpdateSuccess"><i class="fa fa-check"></i> Image name updated</p>
<p class="text-neg text-small" ng-show="imageUpdateFailure"><i class="fa fa-times"></i> <span ng-bind="imageUpdateFailure"></span></p>
</div>
</form>
@ -53,8 +58,6 @@
</form>
</div>
<p class="text-pos" ng-show="imageDeleteSuccess"><i class="fa fa-check"></i> Image deleted</p>
<div class="image-manager-bottom">
<button class="button pos anim fadeIn" ng-show="selectedImage" ng-click="selectButtonClick()">
<i class="zmdi zmdi-square-right"></i>Select Image

View File

@ -1,11 +1,8 @@
@if(Session::has('success'))
<div class="notification anim pos">
<i class="zmdi zmdi-mood"></i> <span>{{ Session::get('success') }}</span>
</div>
@endif
@if(Session::has('error'))
<div class="notification anim neg stopped">
<div class="notification anim pos" @if(!Session::has('success')) style="display:none;" @endif>
<i class="zmdi zmdi-check-circle"></i> <span>{{ Session::get('success') }}</span>
</div>
<div class="notification anim neg stopped" @if(!Session::has('error')) style="display:none;" @endif>
<i class="zmdi zmdi-alert-circle"></i> <span>{{ Session::get('error') }}</span>
</div>
@endif
</div>

View File

@ -28,7 +28,7 @@ class LdapTest extends \TestCase
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
@ -46,6 +46,30 @@ class LdapTest extends \TestCase
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $this->mockUser->name]);
}
public function test_login_works_when_no_uid_provided_by_ldap_server()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setOption')->once();
$ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
->with($this->resourceId, config('services.ldap.base_dn'), Mockery::type('string'), Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'cn' => [$this->mockUser->name],
'dn' => $ldapDn,
'mail' => [$this->mockUser->email]
]]);
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Sign In')
->seePageIs('/')
->see($this->mockUser->name)
->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => 1, 'external_auth_id' => $ldapDn]);
}
public function test_initial_incorrect_details()
{
$this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
@ -55,7 +79,7 @@ class LdapTest extends \TestCase
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test'.config('services.ldap.base_dn')]
'dn' => ['dc=test' . config('services.ldap.base_dn')]
]]);
$this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);