diff --git a/test/api/Mocks.php b/test/api/Mocks.php index 2dc7666..6e4f3f1 100644 --- a/test/api/Mocks.php +++ b/test/api/Mocks.php @@ -21,6 +21,11 @@ class DataMock { $user = R::load('user', $userId); $user->active_token = $jwt; + + if ($userId == 3) { + $user->security_level = SecurityLevel::UNPRIVILEGED; + } + R::store($user); return $jwt; diff --git a/test/api/controllers/AttachmentsTest.php b/test/api/controllers/AttachmentsTest.php index d36c31a..3450c22 100644 --- a/test/api/controllers/AttachmentsTest.php +++ b/test/api/controllers/AttachmentsTest.php @@ -5,6 +5,8 @@ use RedBeanPHP\R; class AttachmentsTest extends PHPUnit\Framework\TestCase { private $attachments; + const diskfilename = '638df56a901567375d06757aeac8317366eb0ade'; + public static function setUpBeforeClass(): void { try { R::setup('sqlite:tests.db'); @@ -77,6 +79,65 @@ class AttachmentsTest extends PHPUnit\Framework\TestCase { $actual->body->data->alerts[0]['text']); } + public function testGetAttachmentByHash() { + $request = new RequestMock(); + $request->header = [DataMock::GetJwt()]; + + $args = []; + $args['hash'] = AttachmentsTest::diskfilename; + + $actual = $this->attachments->getAttachmentByHash($request, + new ResponseMock(), $args); + $this->assertEquals('No attachment found for hash ' . + AttachmentsTest::diskfilename . '.', + $actual->body->data->alerts[0]['text']); + + $this->createAttachment(); + $request->header = [DataMock::GetJwt()]; + + $this->attachments = new Attachments(new LoggerMock()); + + $actual = $this->attachments->getAttachmentByHash($request, + new ResponseMock(), $args); + $this->assertEquals('success', $actual->body->data->status); + $this->assertEquals(2, count($actual->body->data->data)); + } + + public function testGetAttachmentByHashInvalid() { + $request = new RequestMock(); + $request->hasHeader = false; + + $args = []; + $args['hash'] = AttachmentsTest::diskfilename; + + $actual = $this->attachments->getAttachmentByHash($request, + new ResponseMock(), $args); + $this->assertEquals('error', $actual->body->data->alerts[0]['type']); + } + + public function testGetAttachmentByHashForbidden() { + $this->createAttachment(); + + $args = []; + $args['hash'] = AttachmentsTest::diskfilename; + + $actual = $this->attachments->getAttachmentByHash(new RequestMock(), + new ResponseMock(), $args); + $this->assertEquals('error', $actual->body->data->alerts[0]['type']); + + DataMock::CreateBoardAdminUser(); + + $request = new RequestMock(); + $request->header = [DataMock::GetJwt(2)]; + + $this->attachments = new Attachments(new LoggerMock()); + + $actual = $this->attachments->getAttachmentByHash($request, + new ResponseMock(), $args); + $this->assertEquals('Access restricted.', + $actual->body->data->alerts[0]['text']); + } + public function testAddAttachment() { $task = R::dispense('task'); R::store($task); @@ -96,6 +157,8 @@ class AttachmentsTest extends PHPUnit\Framework\TestCase { new ResponseMock(), null); $this->assertEquals('Attachment added.', $actual->body->data->alerts[0]['text']); + + rmdir('uploads/'); } public function testAddAttachmentInvalid() { @@ -107,6 +170,8 @@ class AttachmentsTest extends PHPUnit\Framework\TestCase { new ResponseMock(), null); $this->assertEquals('failure', $actual->body->data->status); $this->assertEquals('error', $actual->body->data->alerts[0]['type']); + + rmdir('uploads/'); } public function testAddAttachmentForbidden() { @@ -136,6 +201,75 @@ class AttachmentsTest extends PHPUnit\Framework\TestCase { new ResponseMock(), null); $this->assertEquals('Access restricted.', $actual->body->data->alerts[0]['text']); + + rmdir('uploads/'); + } + + public function testUploadFile() { + $request = new RequestMock(); + $request->header = [DataMock::GetJwt()]; + + $args = []; + $args['hash'] = AttachmentsTest::diskfilename; + + $actual = $this->attachments->uploadFile($request, + new ResponseMock(), $args); + $this->assertEquals('Error uploading attachment. Please try again.', + $actual->body->data->alerts[0]['text']); + + $this->createAttachment(); + $request->header = [DataMock::GetJwt()]; + + $this->attachments = new Attachments(new LoggerMock()); + + $_FILES['file'] = []; + $_FILES['file']['tmp_name'] = 'asdf'; + $_FILES['file']['error'] = 1; + + $actual = $this->attachments->uploadFile($request, + new ResponseMock(), $args); + $this->assertEquals('failure', $actual->body->data->status); + $this->assertEquals(1, count($actual->body->data->data)); + + $_FILES['file'] = []; + $_FILES['file']['tmp_name'] = 'asdf'; + $_FILES['file']['error'] = 0; + + $this->createAttachment(); + $request->header = [DataMock::GetJwt()]; + + $this->attachments = new Attachments(new LoggerMock()); + + $actual = $this->attachments->uploadFile($request, + new ResponseMock(), $args); + $this->assertEquals('success', $actual->body->data->status); + $this->assertEquals(1, count($actual->body->data->data)); + } + + public function testUploadFileForbidden() { + DataMock::CreateBoardAdminUser(); + $this->createAttachment(); + + $request = new RequestMock(); + $request->header = [DataMock::GetJwt(2)]; + + $args = []; + $args['hash'] = AttachmentsTest::diskfilename; + + $this->attachments = new Attachments(new LoggerMock()); + + $actual = $this->attachments->uploadFile($request, + new ResponseMock(), $args); + $this->assertEquals('Access restricted.', + $actual->body->data->alerts[0]['text']); + + $this->attachments = new Attachments(new LoggerMock()); + $request->header = [DataMock::GetJwt(3)]; + + $actual = $this->attachments->uploadFile($request, + new ResponseMock(), $args); + $this->assertEquals('Insufficient privileges.', + $actual->body->data->alerts[0]['text']); } public function testRemoveAttachment() { @@ -224,6 +358,7 @@ class AttachmentsTest extends PHPUnit\Framework\TestCase { $attachment->name = 'file.png'; $attachment->user_id = 1; + $attachment->diskfilename = AttachmentsTest::diskfilename; $task->xownAttachmentList[] = $attachment; $column->xownTaskList[] = $task; diff --git a/test/api/controllers/UsersTest.php b/test/api/controllers/UsersTest.php index a81becf..1c8ebc4 100644 --- a/test/api/controllers/UsersTest.php +++ b/test/api/controllers/UsersTest.php @@ -94,7 +94,7 @@ class UsersTest extends PHPUnit\Framework\TestCase { $args['id'] = 1; $request = new RequestMock(); - $request->header = [DataMock::GetJwt(3)]; + $request->header = [DataMock::GetJwt(4)]; $this->users = new Users(new LoggerMock()); diff --git a/test/app/board/board.service.spec.ts b/test/app/board/board.service.spec.ts index 93dba3d..5ddc16e 100644 --- a/test/app/board/board.service.spec.ts +++ b/test/app/board/board.service.spec.ts @@ -4,7 +4,7 @@ import { HttpTestingController } from '@angular/common/http/testing'; -import { BoardService } from '../../../src/app/board/board.service'; +import { BoardService } from 'src/app/board/board.service'; describe('BoardService', () => { let injector: TestBed; @@ -16,7 +16,7 @@ describe('BoardService', () => { expect(req.request.method).toEqual(method); if (isError) { - req.flush({ alerts: [{}] }, { status: 500, statusText: '' }); + req.flush({ alerts: [{}], data: [] }, { status: 500, statusText: '' }); } else { req.flush({ data: [] }); } @@ -185,6 +185,22 @@ describe('BoardService', () => { testCall('api/activity/task/1', 'GET', true); }); + it('adds a comment', () => { + service.addComment({ id: 1 }).subscribe(response => { + expect(response.data.length).toEqual(0); + }); + + testCall('api/comments', 'POST', true); + }); + + it('handles errors on comment add', () => { + service.addComment(null).subscribe(() => {}, response => { + expect(response.alerts.length).toEqual(1); + }); + + testCall('api/comments', 'POST'); + }); + it('updates a comment', () => { service.updateComment({ id: 1 }).subscribe(response => { expect(response.data.length).toEqual(0); @@ -217,6 +233,54 @@ describe('BoardService', () => { testCall('api/comments/1', 'DELETE', true); }); + it('adds an attachment', () => { + service.addAttachment({ id: 1 }).subscribe(response => { + expect(response.data.length).toEqual(0); + }); + + testCall('api/attachments', 'POST'); + }); + + it('handles errors on attachment add', () => { + service.addAttachment({ id: 1 }).subscribe(response => { + expect(response.alerts.length).toEqual(1); + }); + + testCall('api/attachments', 'POST', true); + }); + + it('removes an attachment', () => { + service.removeAttachment(1).subscribe(response => { + expect(response.data.length).toEqual(0); + }); + + testCall('api/attachments/1', 'DELETE'); + }); + + it('handles errors on attachment remove', () => { + service.removeAttachment(1).subscribe(response => { + expect(response.alerts.length).toEqual(1); + }); + + testCall('api/attachments/1', 'DELETE', true); + }); + + it('uploads a file', () => { + service.uploadAttachment(null, 'asdf').subscribe(response => { + expect(response.data.length).toEqual(0); + }); + + testCall('api/upload/asdf', 'POST'); + }); + + it('handles errors on file upload', () => { + service.uploadAttachment(null, 'asdf').subscribe(response => { + expect(response.alerts.length).toEqual(1); + }); + + testCall('api/upload/asdf', 'POST', true); + }); + it('refreshes the API token', () => { service.refreshToken(); testCall('api/refresh', 'POST'); diff --git a/test/app/board/column/column.component.spec.ts b/test/app/board/column/column.component.spec.ts index 07e7deb..549b616 100644 --- a/test/app/board/column/column.component.spec.ts +++ b/test/app/board/column/column.component.spec.ts @@ -2,6 +2,7 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { FormsModule } from '@angular/forms'; +import { DomSanitizer } from '@angular/platform-browser'; import { ColumnDisplayComponent } from '../../../../src/app/board/column/column.component'; import { TaskDisplayComponent } from '../../../../src/app/board/task/task.component'; @@ -36,18 +37,26 @@ describe('ColumnDisplay', () => { HttpClientTestingModule, FormsModule, RouterTestingModule, - SharedModule + SharedModule, ], declarations: [ ColumnDisplayComponent, - TaskDisplayComponent + TaskDisplayComponent, ], providers: [ AuthService, NotificationsService, ModalService, StringsService, - BoardService + BoardService, + { + provide: DomSanitizer, + useValue: { + sanitize: (_: any, val: string) => val, + bypassSecurityTrustResourceUrl: (val: string) => val, + bypassSecurityTrustHtml: (val: string) => val, + }, + }, ] }).compileComponents(); }); @@ -72,6 +81,13 @@ describe('ColumnDisplay', () => { expect(component.templateElement.classList.contains('collapsed')).toEqual(true); }); + it('gets a username', () => { + component.activeBoard = { users: [{ id: 1, username: 'hi' }] }; + const username = component.userName(1); + + expect(username).toEqual('hi'); + }); + it('sorts tasks', () => { component.columnData = { tasks: [ { position: 2, due_date: '1/1/2018', points: 1 }, @@ -93,7 +109,7 @@ describe('ColumnDisplay', () => { it('calls a service to toggle collapsed state', () => { (component.boardService.toggleCollapsed as any) = () => { - return { subscribe: (fn: any) => fn({ data: [{}, [1]] } as any) }; + return { subscribe: (fn: any) => fn({ data: [{}, [1]] } as any) }; }; component.activeUser = { id: 1, collapsed: [] } as any; component.columnData = { id: 1 } as any; @@ -121,11 +137,13 @@ describe('ColumnDisplay', () => { }); it('calls a service to add a task', () => { + component.columnData = { id: 1 } as any; + component.addTask(); expect(component.saving).toEqual(false); (component.boardService.addTask as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; }; component.modalProps = { title: 'Testing' } as any; @@ -133,7 +151,12 @@ describe('ColumnDisplay', () => { expect(component.saving).toEqual(false); (component.boardService.addTask as any) = () => { - return { subscribe: (fn: any) => fn({ + return { subscribe: (_: any, err: any) => { err('Err'); } } + } + component.addTask(); + + (component.boardService.addTask as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'success', alerts: [], data: [{}, {}, [{ ownColumn: [{}] }]] @@ -142,14 +165,37 @@ describe('ColumnDisplay', () => { component.addTask(); expect(component.saving).toEqual(false); - }); + }); + + it('handles drop events', () => { + const prev = { data: {} }; + const evt = { + currentIndex: 0, + previousContainer: prev, + container: prev + } + + component.activeBoard = { + columns: [{ id: 1, tasks: [{ id: 1 }, { id: 2 }] }, { id: 3 }] + } as any; + component.moveItemInArray = () => true; + component.transferArrayItem = () => true; + + component.drop(evt as any, 0); + expect(component.activeBoard.columns[0].tasks[0].position).toEqual(1); + + evt.previousContainer = { data: {} }; + + component.drop(evt as any, 0); + expect(component.activeBoard.columns[0].tasks[0].position).toEqual(1); + }); it('calls a service to update a task', () => { component.updateTask(); expect(component.saving).toEqual(false); (component.boardService.updateTask as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; }; component.modalProps = { title: 'Testing' } as any; @@ -157,7 +203,7 @@ describe('ColumnDisplay', () => { expect(component.saving).toEqual(false); (component.boardService.updateTask as any) = () => { - return { subscribe: (fn: any) => fn({ + return { subscribe: (fn: any) => fn({ status: 'success', alerts: [], data: [{}, {}, [{ ownColumn: [{}] }]] @@ -197,6 +243,86 @@ describe('ColumnDisplay', () => { expect(called).toEqual(true); }); + it('handles file input changes', () => { + const file = { test: true }; + + component.fileChange(file as any); + expect(component.fileUpload).toEqual(file); + }); + + it('calls a service to add a file', () => { + let called = false; + + component.notes.noteAdded.subscribe(() => { called = true; }); + component.addFile(); + + expect(called).toEqual(true); + + component.fileUpload = { + name: 'test.png', + type: 'image/png', + }; + component.activeUser = { id: 1 } as any; + component.viewModalProps = { id: 1, attachments: [] } as any; + + (component.boardService.addAttachment as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'error' } as any) }; + }; + + component.addFile(); + expect(component.fileUploading).toEqual(false); + + (component.boardService.addAttachment as any) = () => { + return { subscribe: (fn: any) => fn({ + status: 'success', + data: [{}, { id: 3, diskfilename: 'asdfghjkl' }], + alerts: [{}] + }) }; + }; + + (component.boardService.uploadAttachment as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'success', alerts: [{}] }) }; + }; + + component.addFile(); + expect(component.fileUploading).toEqual(false); + }); + + it('opens a file viewer in a new window', () => { + spyOn(window, 'open'); + + component.viewFile('asdf'); + + expect(window.open).toHaveBeenCalledWith('./files/asdf', 'tb-file-view'); + }); + + it('provides a way to get a the URL for a file', () => { + const url = component.getUrl('asdf').toString(); + + expect(url).toEqual('./api/uploads/asdf'); + }); + + it('calls a service to remove a file', () => { + (component.boardService.removeAttachment as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [] }) }; + }; + + component.attachmentToRemove = { id: 0 } as any; + component.removeAttachment(); + + (component.boardService.removeAttachment as any) = () => { + return { subscribe: (fn: any) => fn({ + status: 'success', + alerts: [{}], + }) }; + }; + + component.viewModalProps.attachments = [{ id: 0 } as any]; + component.removeAttachment(); + + expect(component.viewModalProps.attachments.length).toEqual(0); + }); + it('calls a service to add a comment', () => { component.viewModalProps.id = 0; component.addComment(); @@ -208,21 +334,22 @@ describe('ColumnDisplay', () => { expect(component.newComment).toEqual(''); - (component.boardService.updateTask as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'error' } as any) }; + (component.boardService.addComment as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [] } as any) }; }; + component.newComment = 'Testing.'; component.addComment(); expect(component.newComment).toEqual(''); - (component.boardService.updateTask as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'success', data: [{}, [ + (component.boardService.addComment as any) = () => { + return { subscribe: (fn: any) => fn({ status: 'success', data: [{}, [ mockTask, { id: 2 } - ]] } as any) }; + ]], alerts: [{}] } as any) }; }; component.activeBoard = { - columns: [{ id: 1, tasks: [{ id: 1 }, { id: 2 }] }, { id: 2 }] + columns: [{ id: 1, tasks: [{ id: 1 }, { id: 2 }] }, { id: 3 }] } as any; component.addComment(); @@ -239,7 +366,7 @@ describe('ColumnDisplay', () => { component.commentEdit = { is_edited: false, user_id: 0 } as any; (component.boardService.updateComment as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; }; component.editComment(); @@ -250,7 +377,7 @@ describe('ColumnDisplay', () => { } as any; (component.boardService.updateComment as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'success', alerts: [{}], + return { subscribe: (fn: any) => fn({ status: 'success', alerts: [{}], data: [{}, [mockTask]] } as any) }; }; @@ -271,7 +398,7 @@ describe('ColumnDisplay', () => { } as any; (component.boardService.removeComment as any) = () => { - return { subscribe: (fn: any) => fn({ alerts: [{}], + return { subscribe: (fn: any) => fn({ alerts: [{}], data: [{}, [mockTask]] } as any) }; }; @@ -310,14 +437,14 @@ describe('ColumnDisplay', () => { component.taskLimit = 2; (component.boardService.updateColumn as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; + return { subscribe: (fn: any) => fn({ status: 'error', alerts: [{}] } as any) }; }; component.saveLimitChanges(); expect(component.columnData.task_limit).toEqual(3); (component.boardService.updateColumn as any) = () => { - return { subscribe: (fn: any) => fn({ status: 'success', alerts: [], data: [ + return { subscribe: (fn: any) => fn({ status: 'success', alerts: [], data: [ {}, [{ id: 1, name: 'test', position: 1, board_id: 1, task_limit: 2, ownTask: [] @@ -431,4 +558,32 @@ describe('ColumnDisplay', () => { expect(component.viewModalProps.column_id).toEqual(1); }); + it('gets a comment converted from markdown', () => { + component.activeBoard = { issue_trackers: [] } as any; + + const comment = component.getComment('# Testing'); + + expect(comment).toEqual('

Testing

\n'); + }); + + it('gets a username by user id', () => { + component.activeBoard = { + users: [{ id: 1, username: 'test' } as any] + } as any; + + const uname = component.getUserName(1); + + expect(uname).toEqual('test'); + }); + + it('can call board update with an emitter', () => { + let called = false; + + component.onUpdateBoards.subscribe(() => { called = true; }); + + component.callBoardUpdate(); + + expect(called).toEqual(true); + }); + }); diff --git a/test/app/files/file-viewer.component.spec.ts b/test/app/files/file-viewer.component.spec.ts new file mode 100644 index 0000000..9c2b2c8 --- /dev/null +++ b/test/app/files/file-viewer.component.spec.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { FileViewerComponent } from 'src/app/files/file-viewer.component'; +import { FileViewerService } from 'src/app/files/file-viewer.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NotificationsService, AuthService } from 'src/app/shared/services'; +import { SharedModule } from 'src/app/shared/shared.module'; + +describe('FileViewer', () => { + let component: FileViewerComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + RouterTestingModule, + SharedModule, + ], + declarations: [ + FileViewerComponent, + ], + providers: [ + AuthService, + FileViewerService, + NotificationsService, + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileViewerComponent); + component = fixture.componentInstance; + }); + + it('can be constructed', () => { + expect(component).toBeTruthy(); + }); + + it('implements ngOnInit', () => { + (component.service.getAttachmentInfo as any) = () => { + return { subscribe: (fn: any) => fn({ + alerts: [{}], + data: [{}, { diskfilename: 'asdf' }], + status: 'success' + }) } + } + + component.ngOnInit(); + + expect(component.isLoaded).toEqual(true); + expect(component.fileUrl).toBeTruthy(); + }); +}); + diff --git a/test/app/files/file-viewer.service.spec.ts b/test/app/files/file-viewer.service.spec.ts new file mode 100644 index 0000000..aaa9375 --- /dev/null +++ b/test/app/files/file-viewer.service.spec.ts @@ -0,0 +1,61 @@ +import { TestBed, getTestBed } from '@angular/core/testing'; +import { + HttpTestingController, + HttpClientTestingModule +} from '@angular/common/http/testing'; + +import { FileViewerService } from 'src/app/files/file-viewer.service'; + +describe('FileViewerService', () => { + let injector: TestBed; + let service: FileViewerService; + let httpMock: HttpTestingController; + + const testCall = (url: string, method: string, isError = false) => { + const req = httpMock.expectOne(url); + expect(req.request.method).toEqual(method); + + if (isError) { + req.flush({ alerts: [{}], data: [] }, { status: 500, statusText: '' }); + } else { + req.flush({ data: [] }); + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [FileViewerService] + }); + + injector = getTestBed(); + service = injector.get(FileViewerService); + httpMock = injector.get(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify() + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('gets attachment info', () => { + service.getAttachmentInfo('asdf').subscribe(response => { + expect(response.data.length).toEqual(0); + }); + + testCall('api/attachments/hash/asdf', 'GET'); + }); + + it('handles errors when getting all boards', () => { + service.getAttachmentInfo('asdf').subscribe(() => {}, response => { + expect(response.alerts.length).toEqual(1); + }); + + testCall('api/attachments/hash/asdf', 'GET', true); + }); + +}); +