How to use obs method in wpt

Best JavaScript code snippet using wpt

app.observations.test.ts

Source:app.observations.test.ts Github

copy

Full Screen

1import { Substitute as Sub, Arg, SubstituteOf } from '@fluffy-spoon/substitute'2import { expect } from 'chai'3import uniqid from 'uniqid'4import * as api from '../../../lib/app.api/observations/app.api.observations'5import { AllocateObservationId, registerDeleteRemovedAttachmentsHandler, ReadAttachmentContent, SaveObservation, StoreAttachmentContent } from '../../../lib/app.impl/observations/app.impl.observations'6import { copyMageEventAttrs, MageEvent } from '../../../lib/entities/events/entities.events'7import { addAttachment, Attachment, AttachmentContentPatchAttrs, AttachmentCreateAttrs, AttachmentsRemovedDomainEvent, AttachmentStore, AttachmentStoreError, AttachmentStoreErrorCode, copyAttachmentAttrs, copyObservationAttrs, copyObservationStateAttrs, EventScopedObservationRepository, Observation, ObservationAttrs, ObservationDomainEventType, ObservationEmitted, ObservationRepositoryError, ObservationRepositoryErrorCode, ObservationState, patchAttachment, putAttachmentThumbnailForMinDimension, removeAttachment, removeFormEntry, StagedAttachmentContentRef } from '../../../lib/entities/observations/entities.observations'8import { permissionDenied, MageError, ErrPermissionDenied, ErrEntityNotFound, EntityNotFoundError, InvalidInputError, ErrInvalidInput, PermissionDeniedError, InfrastructureError, ErrInfrastructure } from '../../../lib/app.api/app.api.errors'9import { FormFieldType } from '../../../lib/entities/events/entities.events.forms'10import _ from 'lodash'11import { User, UserId, UserRepository } from '../../../lib/entities/users/entities.users'12import { pipeline, Readable } from 'stream'13import util from 'util'14import { BufferWriteable } from '../../utils'15import EventEmitter from 'events'16describe('observations use case interactions', function() {17 let mageEvent: MageEvent18 let obsRepo: SubstituteOf<EventScopedObservationRepository>19 let userRepo: SubstituteOf<UserRepository>20 let permissions: SubstituteOf<api.ObservationPermissionService>21 let context: api.ObservationRequestContext22 let principalHandle: SubstituteOf<{ requestingPrincipal(): string }>23 beforeEach(function() {24 mageEvent = new MageEvent({25 id: Date.now(),26 acl: {},27 feedIds: [],28 forms: [],29 layerIds: [],30 name: 'Observation App Layer Tests',31 style: {}32 })33 obsRepo = Sub.for<EventScopedObservationRepository>()34 userRepo = Sub.for<UserRepository>()35 permissions = Sub.for<api.ObservationPermissionService>()36 principalHandle = Sub.for<{ requestingPrincipal(): string }>()37 context = {38 mageEvent,39 userId: uniqid(),40 deviceId: uniqid(),41 observationRepository: obsRepo,42 requestToken: uniqid(),43 requestingPrincipal() { return principalHandle.requestingPrincipal() },44 locale() { return null }45 }46 })47 describe('external view of observation', function() {48 it('omits attachment thumbnails', function() {49 const from: ObservationAttrs = {50 id: uniqid(),51 eventId: 987,52 createdAt: new Date(),53 lastModified: new Date(),54 type: 'Feature',55 geometry: { type: 'Point', coordinates: [ 55, 66 ] },56 properties: { timestamp: new Date(), forms: [] },57 states: [],58 favoriteUserIds: [],59 attachments: [60 {61 id: uniqid(),62 observationFormId: uniqid(),63 fieldName: 'field1',64 oriented: false,65 thumbnails: [66 { minDimension: 100 },67 { minDimension: 200 },68 ]69 },70 {71 id: uniqid(),72 observationFormId: uniqid(),73 fieldName: 'field10',74 oriented: false,75 thumbnails: [76 { minDimension: 100 },77 { minDimension: 200 },78 ]79 }80 ]81 }82 const exo = api.exoObservationFor(from)83 expect(exo.attachments).to.have.length(2)84 expect(exo.attachments.map(omitUndefinedFrom)).to.deep.equal([85 {86 id: from.attachments[0].id,87 observationFormId: from.attachments[0].observationFormId,88 fieldName: 'field1',89 oriented: false,90 contentStored: false,91 },92 {93 id: from.attachments[1].id,94 observationFormId: from.attachments[1].observationFormId,95 fieldName: 'field10',96 oriented: false,97 contentStored: false,98 }99 ])100 })101 it('adds populated user display name for creator and important flag', function() {102 const from: ObservationAttrs = {103 id: uniqid(),104 eventId: 987,105 createdAt: new Date(),106 lastModified: new Date(),107 type: 'Feature',108 geometry: { type: 'Point', coordinates: [ 55, 66 ] },109 properties: { timestamp: new Date(), forms: [] },110 userId: uniqid(),111 states: [],112 favoriteUserIds: [],113 important: {114 userId: uniqid(),115 timestamp: new Date(),116 description: 'populate the user',117 },118 attachments: []119 }120 const creator = { id: from.userId!, displayName: 'Creator Test' } as User121 const importantFlagger = { id: from.important?.userId!, displayName: 'Important Flagger Test' } as User122 const exo = api.exoObservationFor(from, { creator, importantFlagger })123 expect(exo.userId).to.equal(from.userId)124 expect(exo.user).to.deep.equal(creator)125 expect(exo.important?.userId).to.equal(from.important?.userId)126 expect(exo.important?.user).to.deep.equal(importantFlagger)127 })128 it('does not populate creator user if the given id does not match', function() {129 const from: ObservationAttrs = {130 id: uniqid(),131 eventId: 987,132 createdAt: new Date(),133 lastModified: new Date(),134 type: 'Feature',135 geometry: { type: 'Point', coordinates: [ 55, 66 ] },136 properties: { timestamp: new Date(), forms: [] },137 userId: uniqid(),138 states: [],139 favoriteUserIds: [],140 attachments: []141 }142 const creator = { id: uniqid(), displayName: 'Creator Mismatch' } as User143 const exo = api.exoObservationFor(from, { creator })144 expect(exo.userId).to.equal(from.userId)145 expect(exo.user).to.be.undefined146 })147 it('does not populate creator user if the observation does not have a creator', function() {148 const from: ObservationAttrs = {149 id: uniqid(),150 eventId: 987,151 createdAt: new Date(),152 lastModified: new Date(),153 type: 'Feature',154 geometry: { type: 'Point', coordinates: [ 55, 66 ] },155 properties: { timestamp: new Date(), forms: [] },156 states: [],157 favoriteUserIds: [],158 attachments: []159 }160 const creator = { id: uniqid(), displayName: 'Creator Mismatch' } as User161 const exo = api.exoObservationFor(from, { creator })162 expect(exo.userId).to.be.undefined163 expect(exo.user).to.be.undefined164 })165 it('does not populate important flag user if given id does not match', function() {166 const from: ObservationAttrs = {167 id: uniqid(),168 eventId: 987,169 createdAt: new Date(),170 lastModified: new Date(),171 type: 'Feature',172 geometry: { type: 'Point', coordinates: [ 55, 66 ] },173 properties: { timestamp: new Date(), forms: [] },174 states: [],175 favoriteUserIds: [],176 important: {177 userId: uniqid(),178 timestamp: new Date(),179 description: 'populate the user',180 },181 attachments: []182 }183 const importantFlagger = { id: from.important?.userId!, displayName: 'Important Flagger Test' } as User184 const exo = api.exoObservationFor(from, { importantFlagger })185 expect(exo.important?.userId).to.equal(from.important?.userId)186 expect(exo.important?.user).to.deep.equal(importantFlagger)187 })188 it('does not populate important flag user if important flag has no user', function() {189 const from: ObservationAttrs = {190 id: uniqid(),191 eventId: 987,192 createdAt: new Date(),193 lastModified: new Date(),194 type: 'Feature',195 geometry: { type: 'Point', coordinates: [ 55, 66 ] },196 properties: { timestamp: new Date(), forms: [] },197 states: [],198 favoriteUserIds: [],199 important: {200 timestamp: new Date(),201 description: 'populate the user',202 },203 attachments: []204 }205 const importantFlagger = { id: uniqid(), displayName: 'Important Flagger Test' } as User206 const exo = api.exoObservationFor(from, { importantFlagger })207 expect(exo.important?.userId).to.be.undefined208 expect(exo.important?.user).to.be.undefined209 })210 it('does not populate important flag user if the observation has no important flag', function() {211 const from: ObservationAttrs = {212 id: uniqid(),213 eventId: 987,214 createdAt: new Date(),215 lastModified: new Date(),216 type: 'Feature',217 geometry: { type: 'Point', coordinates: [ 55, 66 ] },218 properties: { timestamp: new Date(), forms: [] },219 states: [],220 favoriteUserIds: [],221 attachments: []222 }223 const importantFlagger = { id: from.important?.userId!, displayName: 'Important Flagger Test' } as User224 const exo = api.exoObservationFor(from, { importantFlagger })225 expect(exo.important).to.be.undefined226 })227 it('maps undefined important flag', function() {228 const from: ObservationAttrs = {229 id: uniqid(),230 eventId: 987,231 createdAt: new Date(),232 lastModified: new Date(),233 type: 'Feature',234 geometry: { type: 'Point', coordinates: [ 55, 66 ] },235 properties: { timestamp: new Date(), forms: [] },236 states: [],237 favoriteUserIds: [],238 attachments: []239 }240 const exo = api.exoObservationFor(from)241 expect(exo.important).to.be.undefined242 })243 it('maps only the most recent state', function() {244 const from: ObservationAttrs = {245 id: uniqid(),246 eventId: 987,247 createdAt: new Date(),248 lastModified: new Date(),249 type: 'Feature',250 geometry: { type: 'Point', coordinates: [ 55, 66 ] },251 properties: { timestamp: new Date(), forms: [] },252 states: [],253 favoriteUserIds: [],254 attachments: []255 }256 let exo = api.exoObservationFor(from)257 expect(exo).not.to.have.property('states')258 expect(exo.state).to.be.undefined259 const states: ObservationState[] = [260 { id: uniqid(), name: 'archived', userId: uniqid() },261 { id: uniqid(), name: 'active', userId: uniqid() }262 ]263 from.states = states.map(copyObservationStateAttrs)264 exo = api.exoObservationFor(from)265 expect(exo).not.to.have.property('states')266 expect(exo.state).to.deep.equal({ id: states[0].id, name: 'archived', userId: states[0].userId })267 })268 it('sets content stored flag on attachments according to presence of content locator', async function() {269 const from: ObservationAttrs = {270 id: uniqid(),271 eventId: 987,272 createdAt: new Date(),273 lastModified: new Date(),274 type: 'Feature',275 geometry: { type: 'Point', coordinates: [ 55, 66 ] },276 properties: { timestamp: new Date(), forms: [] },277 states: [],278 favoriteUserIds: [],279 attachments: [280 { id: uniqid(), observationFormId: uniqid(), fieldName: 'field1', oriented: false, thumbnails: [], contentLocator: void(0) },281 { id: uniqid(), observationFormId: uniqid(), fieldName: 'field1', oriented: false, thumbnails: [], contentLocator: 'over there' },282 { id: uniqid(), observationFormId: uniqid(), fieldName: 'field1', oriented: false, thumbnails: [] },283 ]284 }285 const exo = api.exoObservationFor(from)286 expect(exo.attachments[0].contentStored).to.be.false287 expect(exo.attachments[1].contentStored).to.be.true288 expect(exo.attachments[2].contentStored).to.be.false289 })290 it('overrides attachment attributes with given thumbnail attributes', async function() {291 const att: Required<Attachment> = {292 id: uniqid(),293 observationFormId: uniqid(),294 fieldName: 'field1',295 contentLocator: uniqid(),296 contentType: 'image/png',297 width: 900,298 height: 1200,299 lastModified: new Date(),300 name: 'rainbow.png',301 oriented: false,302 size: 9879870,303 thumbnails: [304 { minDimension: 200, contentLocator: void(0), size: 2000, contentType: 'image/jpeg', width: 200, height: 300, name: 'rainbow@200.jpg' },305 { minDimension: 300, contentLocator: void(0), size: 3000, contentType: 'image/jpeg', width: 300, height: 400, name: 'rainbow@300.jpg' },306 { minDimension: 400, contentLocator: void(0), size: 4000, contentType: 'image/jpeg', width: 400, height: 500, name: 'rainbow@400.jpg' },307 ]308 }309 expect(api.exoAttachmentForThumbnail(0, att)).to.deep.equal({310 ...api.exoAttachmentFor(att),311 size: 2000,312 width: 200,313 height: 300,314 contentType: 'image/jpeg'315 })316 expect(api.exoAttachmentForThumbnail(1, att)).to.deep.equal({317 ...api.exoAttachmentFor(att),318 size: 3000,319 width: 300,320 height: 400,321 contentType: 'image/jpeg'322 })323 expect(api.exoAttachmentForThumbnail(2, att)).to.deep.equal({324 ...api.exoAttachmentFor(att),325 size: 4000,326 width: 400,327 height: 500,328 contentType: 'image/jpeg'329 })330 const notStored: Attachment = { ...copyAttachmentAttrs(att), contentLocator: undefined }331 notStored.thumbnails[0].contentLocator = uniqid()332 expect(api.exoAttachmentForThumbnail(0, notStored)).to.have.property('contentStored', false)333 })334 })335 describe('allocating observation ids', function() {336 let allocateObservationId: api.AllocateObservationId337 beforeEach(function() {338 allocateObservationId = AllocateObservationId(permissions)339 })340 it('fails without permission', async function() {341 permissions.ensureCreateObservationPermission(Arg.all()).resolves(permissionDenied('create observation', 'test1'))342 const res = await allocateObservationId({ context })343 expect(res.success).to.be.null344 expect(res.error).to.be.instanceOf(MageError)345 expect(res.error?.code).to.equal(ErrPermissionDenied)346 permissions.received(1).ensureCreateObservationPermission(context)347 obsRepo.didNotReceive().allocateObservationId()348 })349 it('gets a new observation id from the context repository', async function() {350 const id = uniqid()351 permissions.ensureCreateObservationPermission(Arg.all()).resolves(null)352 obsRepo.allocateObservationId().resolves(id)353 const res = await allocateObservationId({ context })354 expect(res.error).to.be.null355 expect(res.success).to.equal(id)356 })357 it.skip('TODO: handles rejected promises')358 })359 describe('saving observations', function() {360 let saveObservation: api.SaveObservation361 let minimalObs: ObservationAttrs362 beforeEach(function() {363 saveObservation = SaveObservation(permissions, userRepo)364 minimalObs = {365 id: uniqid(),366 eventId: mageEvent.id,367 createdAt: new Date(),368 lastModified: new Date(),369 type: 'Feature',370 geometry: { type: 'Point', coordinates: [ 13, 57 ] },371 properties: {372 timestamp: new Date(),373 forms: []374 },375 states: [],376 favoriteUserIds: [],377 attachments: [],378 }379 permissions.ensureCreateObservationPermission(Arg.all()).resolves(null)380 permissions.ensureUpdateObservationPermission(Arg.all()).resolves(null)381 })382 it('does not save when the obsevation event id does not match the context event', async function() {383 const eventIdOverride = mageEvent.id * 3384 const req: api.SaveObservationRequest = {385 context,386 observation: { ...observationModFor(minimalObs), eventId: eventIdOverride } as any387 }388 obsRepo.findById(Arg.any()).resolves(null)389 obsRepo.save(Arg.any()).resolves(new ObservationRepositoryError(ObservationRepositoryErrorCode.InvalidObservation))390 const res = await saveObservation(req)391 const err = res.error as InvalidInputError392 expect(res.success).to.be.null393 expect(err.code).to.equal(ErrInvalidInput)394 obsRepo.received(1).save(Arg.any())395 })396 it('populates the creator user id when present', async function() {397 const creator: User = {398 id: context.userId,399 active: true,400 enabled: true,401 authenticationId: 'auth1',402 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),403 displayName: 'Populate Me',404 lastUpdated: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),405 phones: [],406 roleId: uniqid(),407 username: 'populate.me',408 }409 const req: api.SaveObservationRequest = {410 context,411 observation: observationModFor(minimalObs)412 }413 const obsAfter = Observation.evaluate({414 ...minimalObs,415 userId: creator.id416 }, mageEvent)417 obsRepo.findById(Arg.all()).resolves(null)418 obsRepo.save(Arg.all()).resolves(obsAfter)419 userRepo.findAllByIds([ creator.id ]).resolves({ [creator.id]: creator })420 const res = await saveObservation(req)421 const saved = res.success as api.ExoObservation422 expect(res.error).to.be.null423 expect(obsAfter.validation.hasErrors).to.be.false424 expect(saved.user).to.deep.equal({ id: context.userId, displayName: creator.displayName })425 })426 it('populates the important flag user id when present', async function() {427 const importantFlagger: User = {428 id: uniqid(),429 active: true,430 enabled: true,431 authenticationId: 'auth1',432 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),433 displayName: 'Populate Me',434 lastUpdated: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),435 phones: [],436 roleId: uniqid(),437 username: 'populate.me',438 }439 const req: api.SaveObservationRequest = {440 context,441 observation: observationModFor(minimalObs)442 }443 const obsAfter = Observation.evaluate({444 ...minimalObs,445 important: {446 userId: importantFlagger.id,447 description: 'populate the user who flagged this observation',448 timestamp: new Date(Date.now() - 1000 * 60 * 15)449 }450 }, mageEvent)451 obsRepo.findById(Arg.all()).resolves(null)452 obsRepo.save(Arg.all()).resolves(obsAfter)453 userRepo.findAllByIds([ importantFlagger.id ]).resolves({ [importantFlagger.id]: importantFlagger })454 const res = await saveObservation(req)455 const saved = res.success as api.ExoObservation456 expect(res.error).to.be.null457 expect(obsAfter.validation.hasErrors).to.be.false458 expect(saved.important?.user).to.deep.equal({ id: importantFlagger.id, displayName: importantFlagger.displayName })459 })460 it('populates the creator and important flag user id when present', async function() {461 const creator: User = {462 id: uniqid(),463 active: true,464 enabled: true,465 authenticationId: 'auth1',466 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),467 displayName: 'I Made This',468 lastUpdated: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),469 phones: [],470 roleId: uniqid(),471 username: 'user1',472 }473 const importantFlagger: User = {474 id: uniqid(),475 active: true,476 enabled: true,477 authenticationId: 'auth1',478 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),479 displayName: 'I Flagged This',480 lastUpdated: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),481 phones: [],482 roleId: uniqid(),483 username: 'user2',484 }485 const req: api.SaveObservationRequest = {486 context,487 observation: observationModFor(minimalObs)488 }489 const obsAfter = Observation.evaluate({490 ...minimalObs,491 userId: creator.id,492 important: {493 userId: importantFlagger.id,494 description: 'populate the user who flagged this observation',495 timestamp: new Date(Date.now() - 1000 * 60 * 15)496 }497 }, mageEvent)498 obsRepo.findById(Arg.all()).resolves(null)499 obsRepo.save(Arg.all()).resolves(obsAfter)500 userRepo.findAllByIds(Arg.any()).resolves({ [creator.id]: creator, [importantFlagger.id]: importantFlagger })501 const res = await saveObservation(req)502 const saved = res.success as api.ExoObservation503 expect(res.error).to.be.null504 expect(obsAfter.validation.hasErrors).to.be.false505 expect(saved.user).to.deep.equal({ id: creator.id, displayName: creator.displayName }, 'creator')506 expect(saved.important?.user).to.deep.equal({ id: importantFlagger.id, displayName: importantFlagger.displayName }, 'important flagger')507 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter, { creator, importantFlagger }), 'saved result')508 userRepo.received(1).findAllByIds(Arg.all())509 userRepo.received(1).findAllByIds(Arg.is((x: UserId[]) => x.length === 2 && x.every(id => [ creator.id, importantFlagger.id ].includes(id))))510 userRepo.didNotReceive().findById(Arg.all())511 })512 describe('creating', function() {513 beforeEach(function() {514 obsRepo.findById(Arg.any()).resolves(null)515 })516 it('ensures create permission when no observation exists', async function() {517 const deny = Sub.for<api.ObservationPermissionService>()518 deny.ensureCreateObservationPermission(Arg.all()).resolves(permissionDenied('test create', context.userId, minimalObs.id))519 saveObservation = SaveObservation(deny, userRepo)520 const req: api.SaveObservationRequest = {521 context,522 observation: observationModFor(minimalObs)523 }524 const res = await saveObservation(req)525 const denied = res.error as PermissionDeniedError526 expect(res.success).to.be.null527 expect(denied).to.be.instanceOf(MageError)528 expect(denied.code).to.equal(ErrPermissionDenied)529 deny.received(1).ensureCreateObservationPermission(context)530 deny.didNotReceive().ensureUpdateObservationPermission(Arg.all())531 obsRepo.didNotReceive().save(Arg.all())532 })533 it('validates the id for a new observation', async function() {534 const req: api.SaveObservationRequest = {535 context,536 observation: observationModFor(minimalObs)537 }538 obsRepo.save(Arg.any()).resolves(new ObservationRepositoryError(ObservationRepositoryErrorCode.InvalidObservationId))539 const res = await saveObservation(req)540 const err = res.error as EntityNotFoundError541 expect(res.success).to.be.null542 expect(err).to.be.instanceOf(MageError)543 expect(err.code).to.equal(ErrEntityNotFound)544 expect(err.data.entityId).to.equal(minimalObs.id)545 expect(err.data.entityType).to.equal('ObservationId')546 obsRepo.received(1).save(Arg.any())547 })548 it('assigns creating user id and device id fron request context', async function() {549 const req: api.SaveObservationRequest = {550 context,551 observation: observationModFor(minimalObs)552 }553 const createdAttrs: ObservationAttrs = {554 ...copyObservationAttrs(minimalObs),555 userId: context.userId,556 deviceId: context.deviceId557 }558 const created = Observation.evaluate(createdAttrs, mageEvent)559 const creator: User = { id: context.userId, displayName: `User ${context.userId}` } as User560 obsRepo.save(Arg.all()).resolves(created)561 userRepo.findAllByIds([ context.userId ]).resolves({ [context.userId]: creator })562 const res = await saveObservation(req)563 const saved = res.success as api.ExoObservation564 expect(res.error).to.be.null565 expect(created.validation.hasErrors).to.be.false566 expect(created.userId).to.equal(context.userId)567 expect(created.deviceId).to.equal(context.deviceId)568 expect(saved).to.deep.equal(api.exoObservationFor(created, { creator }))569 obsRepo.received(1).save(Arg.all())570 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(created)))571 })572 })573 describe('updating', function() {574 let obsBefore: Observation575 beforeEach(function() {576 const eventAttrs = copyMageEventAttrs(mageEvent)577 eventAttrs.forms = [578 {579 id: 135,580 name: 'Form 1',581 archived: false,582 color: '#123456',583 fields: [584 {585 id: 876,586 name: 'field1',587 required: false,588 title: 'Field 1',589 type: FormFieldType.Text,590 },591 {592 id: 987,593 name: 'field2',594 required: false,595 title: 'Field 2',596 type: FormFieldType.Attachment597 }598 ],599 userFields: []600 }601 ]602 mageEvent = new MageEvent(eventAttrs)603 context.mageEvent = mageEvent604 const formId = mageEvent.forms[0].id605 const formEntryId = uniqid()606 obsBefore = Observation.evaluate({607 ...minimalObs,608 userId: uniqid(),609 deviceId: uniqid(),610 important: {611 userId: uniqid(),612 timestamp: new Date(minimalObs.lastModified.getTime() - 1000 * 60 * 20),613 description: 'oh my goodness look'614 },615 properties: {616 timestamp: minimalObs.properties.timestamp,617 forms: [618 { id: formEntryId, formId, field1: 'existing form entry' }619 ],620 },621 attachments: [622 {623 id: uniqid(),624 observationFormId: formEntryId,625 fieldName: 'field2',626 name: 'photo1.png',627 oriented: false,628 thumbnails: [629 { minDimension: 120, name: 'photo1@120.png' }630 ]631 }632 ]633 }, mageEvent)634 const obsBeforeId = obsBefore.id635 obsRepo.findById(Arg.is(x => x === obsBeforeId)).resolves(obsBefore)636 userRepo.findAllByIds(Arg.all()).resolves({})637 })638 it('ensures update permission when an observation already exists', async function() {639 const deny = Sub.for<api.ObservationPermissionService>()640 deny.ensureUpdateObservationPermission(Arg.all()).resolves(permissionDenied('test update', context.userId, minimalObs.id))641 saveObservation = SaveObservation(deny, userRepo)642 const req: api.SaveObservationRequest = {643 context,644 observation: observationModFor(minimalObs)645 }646 const res = await saveObservation(req)647 const denied = res.error as PermissionDeniedError648 expect(res.success).to.be.null649 expect(denied).to.be.instanceOf(MageError)650 expect(denied.code).to.equal(ErrPermissionDenied)651 deny.received(1).ensureUpdateObservationPermission(context)652 deny.didNotReceive().ensureCreateObservationPermission(Arg.all())653 obsRepo.didNotReceive().save(Arg.all())654 })655 it('preserves creator user id and device id', async function() {656 const modUserId = uniqid()657 const modDeviceId = uniqid()658 const mod: api.ExoObservationMod = {659 id: obsBefore.id,660 userId: modUserId,661 deviceId: modDeviceId,662 type: 'Feature',663 geometry: obsBefore.geometry,664 properties: {665 timestamp: obsBefore.timestamp,666 forms: [ { id: obsBefore.formEntries[0].id, formId: obsBefore.formEntries[0].formId, field1: 'updated field' } ]667 }668 }669 const obsAfterAtrs: ObservationAttrs = {670 ...copyObservationAttrs(obsBefore),671 properties: {672 timestamp: obsBefore.timestamp,673 forms: [ { id: obsBefore.formEntries[0].id, formId: obsBefore.formEntries[0].formId, field1: 'updated field' } ]674 }675 }676 const obsAfter = Observation.assignTo(obsBefore, obsAfterAtrs) as Observation677 const req: api.SaveObservationRequest = { context, observation: mod }678 obsRepo.save(Arg.all()).resolves(obsAfter)679 const res = await saveObservation(req)680 const saved = res.success as api.ExoObservation681 expect(obsBefore.userId).to.exist.and.not.be.empty682 expect(obsBefore.deviceId).to.exist.and.not.be.empty683 expect(context.userId).to.not.equal(obsBefore.userId)684 expect(context.deviceId).to.not.equal(obsBefore.deviceId)685 expect(mod.userId).to.not.equal(obsBefore.userId)686 expect(mod.deviceId).to.not.equal(obsBefore.deviceId)687 expect(obsAfter.validation.hasErrors, 'valid update').to.be.false688 expect(obsAfter.userId).to.equal(obsBefore.userId)689 expect(obsAfter.deviceId).to.equal(obsBefore.deviceId)690 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter))691 obsRepo.received(1).save(Arg.all())692 obsRepo.received(1).save(Arg.is(validObservation()))693 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter)))694 })695 it('obtains ids for new form entries', async function() {696 const nextEntryId = uniqid()697 const obsAfter = Observation.evaluate({698 ...copyObservationAttrs(obsBefore),699 properties: {700 timestamp: obsBefore.properties.timestamp,701 forms: [702 ...obsBefore.properties.forms,703 { id: nextEntryId, formId: mageEvent.forms[0].id, field1: 'new form entry' }704 ]705 },706 }, mageEvent)707 const obsMod: api.ExoObservationMod = {708 ...copyObservationAttrs(obsBefore),709 properties: {710 timestamp: minimalObs.properties.timestamp,711 forms: [712 { ...obsBefore.properties.forms[0] },713 { id: 'next1', formId: mageEvent.forms[0].id, field1: 'new form entry' }714 ]715 }716 }717 const req: api.SaveObservationRequest = {718 context,719 observation: obsMod720 }721 obsRepo.nextFormEntryIds(1).resolves([ nextEntryId ])722 obsRepo.save(Arg.all()).resolves(obsAfter)723 const res = await saveObservation(req)724 const saved = res.success as api.ExoObservation725 expect(res.error).to.be.null726 expect(obsAfter.validation.hasErrors).to.be.false727 expect(saved.properties.forms[0]).to.deep.equal(obsBefore.properties.forms[0])728 expect(saved.properties.forms[1]).to.deep.equal({729 id: nextEntryId,730 formId: mageEvent.forms[0].id,731 field1: 'new form entry'732 })733 obsRepo.received(1).nextFormEntryIds(Arg.all())734 obsRepo.received(1).save(Arg.is(validObservation()))735 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter)))736 obsRepo.received(1).save(Arg.all())737 })738 it('obtains ids for new attachments', async function() {739 /*740 do clients send a complete array of attachments for the form fields or741 only the attachments with actions?742 answer: web app just sends the action attachments743 */744 const nextAttachmentId = uniqid()745 const newAttachment: AttachmentCreateAttrs = {746 oriented: false,747 thumbnails: [],748 size: 123678,749 name: 'new attachment.png'750 }751 const obsAfter = addAttachment(obsBefore, nextAttachmentId, 'field2', obsBefore.formEntries[0].id, newAttachment) as Observation752 const obsMod: api.ExoObservationMod = _.omit({753 ...copyObservationAttrs(obsBefore),754 properties: {755 timestamp: minimalObs.properties.timestamp,756 forms: [757 {758 ...obsBefore.properties.forms[0],759 field2: [{760 action: api.AttachmentModAction.Add,761 name: newAttachment.name,762 size: newAttachment.size763 }]764 },765 ]766 }767 }, 'attachments')768 const req: api.SaveObservationRequest = {769 context,770 observation: obsMod771 }772 obsRepo.nextAttachmentIds(1).resolves([ nextAttachmentId ])773 obsRepo.save(Arg.all()).resolves(obsAfter)774 const res = await saveObservation(req)775 const saved = res.success as api.ExoObservation776 expect(res.error).to.be.null777 expect(obsAfter.validation.hasErrors).to.be.false778 expect(saved.properties.forms).to.deep.equal(obsBefore.properties.forms)779 expect(saved.attachments).to.have.length(2)780 expect(saved.attachments.map(omitUndefinedFrom)).to.deep.equal(obsAfter.attachments.map(api.exoAttachmentFor).map(omitUndefinedFrom))781 obsRepo.received(1).nextAttachmentIds(Arg.all())782 obsRepo.received(1).save(Arg.is(validObservation()))783 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))784 obsRepo.received(1).save(Arg.all())785 })786 it('ignores form entry id and field name keys on added attachments', async function() {787 // use only the attachment mod's placement in the form entry788 const nextAttachmentId = uniqid()789 const newAttachment: AttachmentCreateAttrs = {790 oriented: false,791 thumbnails: [],792 name: 'new attachment.png'793 }794 const obsAfter = addAttachment(obsBefore, nextAttachmentId, 'field2', obsBefore.formEntries[0].id, newAttachment) as Observation795 const obsMod: api.ExoObservationMod = _.omit({796 ...copyObservationAttrs(obsBefore),797 properties: {798 timestamp: minimalObs.properties.timestamp,799 forms: [800 {801 ...obsBefore.properties.forms[0],802 field2: [{803 action: api.AttachmentModAction.Add,804 observationFormId: 'invalidFormEntryId',805 fieldName: 'notField2',806 name: newAttachment.name,807 }]808 },809 ]810 }811 }, 'attachments')812 const req: api.SaveObservationRequest = {813 context,814 observation: obsMod815 }816 obsRepo.nextAttachmentIds(1).resolves([ nextAttachmentId ])817 obsRepo.save(Arg.all()).resolves(obsAfter)818 const res = await saveObservation(req)819 const saved = res.success as api.ExoObservation820 expect(res.error).to.be.null821 expect(obsAfter.validation.hasErrors).to.be.false822 expect(saved.properties.forms).to.deep.equal(obsBefore.properties.forms)823 expect(saved.attachments).to.have.length(2)824 expect(saved.attachments[1].name).to.equal('new attachment.png')825 expect(saved.attachments[1].observationFormId).to.equal(obsBefore.formEntries[0].id)826 expect(saved.attachments[1].fieldName).to.equal('field2')827 expect(saved.attachments.map(omitUndefinedFrom)).to.deep.equal(obsAfter.attachments.map(api.exoAttachmentFor).map(omitUndefinedFrom))828 obsRepo.received(1).nextAttachmentIds(Arg.all())829 obsRepo.received(1).save(Arg.is(validObservation()))830 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))831 obsRepo.received(1).save(Arg.all())832 })833 it('ignores the attachments array on the observation', async function() {834 // TODO: for create as well835 const obsMod: api.ExoObservationMod = <any>{836 ...copyObservationAttrs(obsBefore),837 attachments: [838 <Attachment>{ ...obsBefore.attachments[0], size: Number(obsBefore.attachments[0].size) * 2, name: 'do not change.png' },839 <Attachment>{ id: uniqid(), observationFormId: obsBefore.formEntries[0].id, fieldName: 'field2', name: 'cannot add from here.png', oriented: false, thumbnails: [] }840 ]841 }842 const req: api.SaveObservationRequest = {843 context,844 observation: obsMod845 }846 obsRepo.save(Arg.all()).resolves(obsBefore)847 const res = await saveObservation(req)848 const saved = res.success as api.ExoObservation849 expect(res.error).to.be.null850 expect(saved.attachments).to.have.length(1)851 expect(omitUndefinedFrom(saved.attachments[0])).to.deep.equal(omitUndefinedFrom(api.exoAttachmentFor(obsBefore.attachments[0])))852 obsRepo.didNotReceive().nextAttachmentIds(Arg.all())853 obsRepo.received(1).save(Arg.is(validObservation()))854 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsBefore, 'repository save argument')))855 obsRepo.received(1).save(Arg.all())856 })857 it('does not save attachment mods without a corresponding form field', async function() {858 /*859 a client could submit an attachment mod array for a field name that860 does not exist in the event form861 */862 /*863 TODO: this bit should change to invalidate or remove the invalid form864 entry key. currently observation validation only considers form field865 entries that have corresponding fields, while simply ignoring field866 entries without a corresponding field or a field that is archived.867 */868 const obsAfter = Observation.assignTo(obsBefore, {869 ...copyObservationAttrs(obsBefore),870 properties: {871 timestamp: obsBefore.properties.timestamp,872 forms: [873 {874 ...obsBefore.formEntries[0],875 whoops: [ { action: api.AttachmentModAction.Add, name: 'bad field reference.png' } as any ]876 }877 ]878 }879 }) as Observation880 const obsMod: api.ExoObservationMod = _.omit({881 ...copyObservationAttrs(obsBefore),882 properties: {883 timestamp: minimalObs.properties.timestamp,884 forms: [885 {886 ...obsBefore.properties.forms[0],887 whoops: [ { action: api.AttachmentModAction.Add, name: 'bad field reference.png' } ]888 },889 ]890 }891 }, 'attachments')892 const req: api.SaveObservationRequest = {893 context,894 observation: obsMod895 }896 obsRepo.save(Arg.all()).resolves(obsAfter)897 const res = await saveObservation(req)898 const saved = res.success as api.ExoObservation899 expect(res.error).to.be.null900 expect(obsAfter.validation.hasErrors).to.be.false901 expect(obsAfter.attachments.map(copyAttachmentAttrs)).to.deep.equal(obsBefore.attachments.map(copyAttachmentAttrs))902 expect(saved.attachments).to.have.length(1)903 expect(omitUndefinedFrom(saved.attachments[0])).to.deep.equal(omitUndefinedFrom(api.exoAttachmentFor(obsBefore.attachments[0])))904 obsRepo.didNotReceive().nextAttachmentIds(Arg.all())905 obsRepo.received(1).save(Arg.is(validObservation()))906 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))907 obsRepo.received(1).save(Arg.all())908 })909 it('preserves existing attachments when the attachment field entry is null', async function() {910 /*911 for some reason the web client sends a null value for the attachment field912 in form entries other than the one that has a new attachment appended, so913 the server needs to ignore that. for example914 {915 id: 'aq1sw2',916 ...917 properties: {918 forms: [919 {920 ...921 attField1: [ { action: 'add', ... } ],922 attField2: null923 }924 ]925 }926 }927 */928 const obsAfter = Observation.assignTo(obsBefore, obsBefore) as Observation929 const obsMod: api.ExoObservationMod = _.omit({930 ...copyObservationAttrs(obsBefore),931 properties: {932 timestamp: minimalObs.properties.timestamp,933 forms: [934 {935 ...obsBefore.properties.forms[0],936 field2: null937 },938 ]939 }940 }, 'attachments')941 const req: api.SaveObservationRequest = {942 context,943 observation: obsMod944 }945 obsRepo.save(Arg.all()).resolves(obsAfter)946 const res = await saveObservation(req)947 const saved = res.success as api.ExoObservation948 expect(res.error).to.be.null949 expect(obsAfter.validation.hasErrors).to.be.false950 expect(saved.properties.forms).to.deep.equal(obsBefore.properties.forms)951 expect(saved.attachments).to.have.length(1)952 expect(saved.attachments.map(omitUndefinedFrom)).to.deep.equal(obsAfter.attachments.map(api.exoAttachmentFor).map(omitUndefinedFrom))953 obsRepo.didNotReceive().nextAttachmentIds(Arg.all())954 obsRepo.received(1).save(Arg.is(validObservation()))955 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))956 obsRepo.received(1).save(Arg.all())957 })958 it('preserves attachment thumbnails even though app clients do not send them', async function() {959 const obsAfter = Observation.assignTo(obsBefore, {960 ...copyObservationAttrs(obsBefore),961 properties: {962 timestamp: obsBefore.properties.timestamp,963 forms: [964 {965 ...obsBefore.formEntries[0],966 field1: 'mod field 1',967 }968 ]969 }970 }) as Observation971 const obsMod: api.ExoObservationMod = _.omit({972 ...copyObservationAttrs(obsBefore),973 properties: {974 timestamp: minimalObs.properties.timestamp,975 forms: [976 {977 ...obsBefore.formEntries[0],978 field1: 'mod field 1',979 field2: null // no attachment mods980 },981 ]982 }983 }, 'attachments')984 const req: api.SaveObservationRequest = {985 context,986 observation: obsMod987 }988 obsRepo.save(Arg.all()).resolves(obsAfter)989 const res = await saveObservation(req)990 const saved = res.success as api.ExoObservation991 expect(res.error).to.be.null992 expect(obsAfter.validation.hasErrors).to.be.false993 expect(obsBefore.attachments.map(copyAttachmentAttrs)).to.deep.equal([994 copyAttachmentAttrs({995 id: obsBefore.attachments[0].id,996 observationFormId: obsBefore.formEntries[0].id,997 fieldName: 'field2',998 name: 'photo1.png',999 oriented: false,1000 thumbnails: [1001 { minDimension: 120, name: 'photo1@120.png' }1002 ]1003 })1004 ])1005 expect(obsAfter.attachments.map(copyAttachmentAttrs)).to.deep.equal(obsBefore.attachments.map(copyAttachmentAttrs))1006 obsRepo.received(1).save(Arg.is(validObservation()))1007 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))1008 obsRepo.received(1).save(Arg.all())1009 })1010 it('removes form entries and their attachment meta-data', async function() {1011 const obsAfter = removeFormEntry(obsBefore, obsBefore.formEntries[0].id)1012 const obsMod: api.ExoObservationMod = {1013 ...copyObservationAttrs(obsBefore),1014 properties: {1015 timestamp: minimalObs.properties.timestamp,1016 forms: []1017 }1018 }1019 const req: api.SaveObservationRequest = {1020 context,1021 observation: obsMod1022 }1023 obsRepo.save(Arg.all()).resolves(obsAfter)1024 const res = await saveObservation(req)1025 const saved = res.success as api.ExoObservation1026 expect(res.error).to.be.null1027 expect(obsAfter.validation.hasErrors).to.be.false1028 expect(saved.properties.forms).to.be.empty1029 expect(saved.attachments).to.be.empty1030 obsRepo.didNotReceive().nextFormEntryIds(Arg.all())1031 obsRepo.received(1).save(Arg.is(validObservation()))1032 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter)))1033 obsRepo.received(1).save(Arg.all())1034 })1035 it('removes attachments', async function() {1036 const obsAfter = removeAttachment(1037 Observation.assignTo(obsBefore, {1038 ...copyObservationAttrs(obsBefore),1039 properties: {1040 timestamp: obsBefore.timestamp,1041 forms: [1042 {1043 ...obsBefore.formEntries[0],1044 field1: 'mod field 1'1045 }1046 ]1047 }1048 }) as Observation,1049 obsBefore.attachments[0].id) as Observation1050 const obsMod: api.ExoObservationMod = _.omit({1051 ...copyObservationAttrs(obsBefore),1052 properties: {1053 timestamp: minimalObs.properties.timestamp,1054 forms: [1055 {1056 ...obsBefore.formEntries[0],1057 field1: 'mod field 1',1058 field2: [1059 {1060 action: api.AttachmentModAction.Delete,1061 id: obsBefore.attachments[0].id1062 }1063 ]1064 },1065 ]1066 }1067 }, 'attachments')1068 const req: api.SaveObservationRequest = {1069 context,1070 observation: obsMod1071 }1072 obsRepo.save(Arg.all()).resolves(obsAfter)1073 const res = await saveObservation(req)1074 const saved = res.success as api.ExoObservation1075 expect(res.error).to.be.null1076 expect(obsBefore.attachments).to.have.length(1)1077 expect(obsAfter.validation.hasErrors).to.be.false1078 expect(obsAfter.attachments).to.be.empty1079 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter))1080 obsRepo.received(1).save(Arg.is(validObservation()))1081 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))1082 })1083 it('adds and removes attachments', async function() {1084 const nextAttachmentId = uniqid()1085 const addedAttachment: Attachment = { id: nextAttachmentId, observationFormId: obsBefore.formEntries[0].id, fieldName: 'field2', oriented: false, thumbnails: [], name: 'added.png' }1086 const obsAfter = Observation.assignTo(obsBefore, {1087 ...copyObservationAttrs(obsBefore),1088 properties: {1089 timestamp: obsBefore.timestamp,1090 forms: [1091 { ...obsBefore.formEntries[0], field1: 'mod field 1' }1092 ],1093 },1094 attachments: [ addedAttachment ]1095 }) as Observation1096 const obsMod: api.ExoObservationMod = _.omit({1097 ...copyObservationAttrs(obsBefore),1098 properties: {1099 timestamp: minimalObs.properties.timestamp,1100 forms: [1101 {1102 ...obsBefore.formEntries[0],1103 field1: 'mod field 1',1104 field2: [1105 {1106 action: api.AttachmentModAction.Delete,1107 id: obsBefore.attachments[0].id1108 },1109 {1110 action: api.AttachmentModAction.Add,1111 name: addedAttachment.name1112 }1113 ]1114 },1115 ]1116 }1117 }, 'attachments')1118 const req: api.SaveObservationRequest = {1119 context,1120 observation: obsMod1121 }1122 obsRepo.nextAttachmentIds(1).resolves([ nextAttachmentId ])1123 obsRepo.save(Arg.all()).resolves(obsAfter)1124 const res = await saveObservation(req)1125 const saved = res.success as api.ExoObservation1126 expect(res.error).to.be.null1127 expect(obsBefore.attachments).to.have.length(1)1128 expect(obsAfter.validation.hasErrors).to.be.false1129 expect(obsAfter.attachments).to.have.length(1)1130 expect(obsAfter.attachments[0]).to.deep.equal(copyAttachmentAttrs({1131 id: nextAttachmentId,1132 observationFormId: obsBefore.formEntries[0].id,1133 fieldName: 'field2',1134 name: 'added.png',1135 oriented: false,1136 thumbnails: [],1137 }))1138 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter))1139 obsRepo.received(1).nextAttachmentIds(Arg.all())1140 obsRepo.received(1).save(Arg.is(validObservation()))1141 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))1142 })1143 it.skip('TODO: removes attachment content for removed attachments', async function() {1144 expect.fail('todo')1145 })1146 it.skip('TODO: removes attachment content for removed form entries', async function() {1147 expect.fail('todo')1148 })1149 it('preserves the important flag', async function() {1150 const obsAfter = Observation.assignTo(obsBefore, {1151 ...copyObservationAttrs(obsBefore),1152 properties: {1153 timestamp: obsBefore.timestamp,1154 forms: [1155 { ...obsBefore.formEntries[0], field1: 'mod field 1' }1156 ],1157 }1158 }) as Observation1159 const obsMod: api.ExoObservationMod = _.omit({1160 ...copyObservationAttrs(obsBefore),1161 properties: {1162 timestamp: minimalObs.properties.timestamp,1163 forms: [1164 { ...obsBefore.formEntries[0], field1: 'mod field 1' }1165 ]1166 }1167 }, 'attachments')1168 const req: api.SaveObservationRequest = {1169 context,1170 observation: obsMod1171 }1172 obsRepo.save(Arg.all()).resolves(obsAfter)1173 const res = await saveObservation(req)1174 const saved = res.success as api.ExoObservation1175 expect(res.error).to.be.null1176 expect(obsBefore.important).to.have.property('userId').that.is.a('string')1177 expect(obsBefore.important).to.have.property('timestamp').that.is.instanceOf(Date)1178 expect(obsBefore.important).to.have.property('description', 'oh my goodness look')1179 expect(obsAfter.validation.hasErrors).to.be.false1180 expect(obsAfter.important).to.deep.equal(obsBefore.important)1181 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter))1182 obsRepo.received(1).save(Arg.is(validObservation()))1183 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'repository save argument')))1184 })1185 it('preserves states', async function() {1186 obsBefore = Observation.evaluate({1187 ...copyObservationAttrs(obsBefore),1188 id: uniqid(),1189 states: [1190 { id: uniqid(), name: 'active', userId: uniqid() },1191 { id: uniqid(), name: 'archived', userId: uniqid() }1192 ]1193 }, mageEvent) as Observation1194 const obsAfter = Observation.assignTo(obsBefore, {1195 ...copyObservationAttrs(obsBefore),1196 properties: {1197 timestamp: obsBefore.timestamp,1198 forms: [ { ...obsBefore.formEntries[0], field1: 'mod field 1' } ]1199 }1200 }) as Observation1201 const mod: api.ExoObservationMod = {1202 ...observationModFor(obsBefore),1203 properties: {1204 timestamp: obsBefore.timestamp,1205 forms: [ { ...obsBefore.formEntries[0], field1: 'mod field 1' } ]1206 },1207 states: []1208 } as any1209 const req: api.SaveObservationRequest = {1210 context,1211 observation: mod1212 }1213 obsRepo.findById(Arg.is(x => x === obsBefore.id)).resolves(obsBefore)1214 obsRepo.save(Arg.all()).resolves(obsAfter)1215 const res = await saveObservation(req)1216 const saved = res.success as api.ExoObservation1217 expect(res.error).to.be.null1218 expect(obsBefore.states).to.have.length(2)1219 expect(obsAfter.states).to.deep.equal(obsBefore.states)1220 expect(saved).to.deep.equal(api.exoObservationFor(obsAfter), 'saved result')1221 expect(saved.state).to.deep.equal(obsAfter.states[0])1222 obsRepo.received(1).save(Arg.all())1223 obsRepo.received(1).save(Arg.is(equalToObservationIgnoringDates(obsAfter, 'save argument')))1224 })1225 })1226 })1227 describe('saving attachment content', function() {1228 let storeAttachmentContent: api.StoreAttachmentContent1229 let store: SubstituteOf<AttachmentStore>1230 let obs: Observation1231 beforeEach(function() {1232 const permittedUser = context.userId1233 permissions.ensureStoreAttachmentContentPermission(Arg.all()).mimicks(async context => {1234 if (context.userId === permittedUser) {1235 return null1236 }1237 return permissionDenied('update observation', context.userId)1238 })1239 store = Sub.for<AttachmentStore>()1240 mageEvent = new MageEvent({1241 ...copyMageEventAttrs(mageEvent),1242 forms: [1243 {1244 id: 1,1245 name: 'Save Attachment Content',1246 archived: false,1247 color: '#12ab34',1248 fields: [1249 {1250 id: 1,1251 name: 'description',1252 title: 'Description',1253 type: FormFieldType.Text,1254 required: false,1255 },1256 {1257 id: 2,1258 name: 'attachments',1259 title: 'Attachments',1260 type: FormFieldType.Attachment,1261 required: false,1262 }1263 ],1264 userFields: []1265 }1266 ]1267 })1268 const baseObsAttrs: ObservationAttrs = {1269 id: uniqid(),1270 eventId: mageEvent.id,1271 createdAt: new Date(),1272 lastModified: new Date(),1273 type: 'Feature',1274 geometry: { type: 'Point', coordinates: [ 55, 66 ] },1275 properties: {1276 timestamp: new Date(),1277 forms: [1278 { id: 'formEntry1', formId: mageEvent.forms[0].id, description: `something interesting at ${new Date().toISOString()}` }1279 ]1280 },1281 states: [],1282 favoriteUserIds: [],1283 attachments: []1284 }1285 baseObsAttrs.attachments = [1286 {1287 id: uniqid(),1288 observationFormId: 'formEntry1',1289 fieldName: 'attachments',1290 name: 'store test 1.jpg',1291 contentType: 'image/jpeg',1292 size: 12345,1293 oriented: false,1294 thumbnails: [],1295 contentLocator: uniqid()1296 },1297 {1298 id: uniqid(),1299 observationFormId: 'formEntry1',1300 fieldName: 'attachments',1301 name: 'store test 2.jpg',1302 contentType: 'image/jpeg',1303 size: 23456,1304 oriented: false,1305 thumbnails: [],1306 }1307 ]1308 obs = Observation.evaluate(baseObsAttrs, mageEvent)1309 storeAttachmentContent = StoreAttachmentContent(permissions, store)1310 expect(obs.validation.hasErrors).to.be.false1311 })1312 it('checks permission to store the attachment', async function() {1313 const attachment = obs.attachments[0]1314 const forbiddenUser = uniqid()1315 const bytesBuffer = Buffer.from('photo of something')1316 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1317 context.userId = forbiddenUser1318 const content: api.ExoIncomingAttachmentContent = {1319 name: attachment.name!,1320 mediaType: attachment.contentType!,1321 bytes,1322 }1323 const req: api.StoreAttachmentContentRequest = {1324 context,1325 observationId: obs.id,1326 attachmentId: obs.attachments[0].id,1327 content,1328 }1329 obsRepo.findById(obs.id).resolves(obs)1330 const res = await storeAttachmentContent(req)1331 const denied = res.error as PermissionDeniedError1332 expect(res.success).to.be.null1333 expect(denied).to.be.instanceOf(MageError)1334 expect(denied.code).to.equal(ErrPermissionDenied)1335 expect(denied.data.permission).to.equal('update observation')1336 expect(denied.data.subject).to.equal(forbiddenUser)1337 store.didNotReceive().saveContent(Arg.all())1338 obsRepo.didNotReceive().save(Arg.all())1339 permissions.received(1).ensureStoreAttachmentContentPermission(Arg.all())1340 permissions.received(1).ensureStoreAttachmentContentPermission(context, obs, obs.attachments[0].id)1341 })1342 it('saves the attachment content to the attachment store', async function() {1343 const attachment = obs.attachments[0]1344 const bytesBuffer = Buffer.from('photo of something')1345 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1346 const content: api.ExoIncomingAttachmentContent = {1347 name: attachment.name!,1348 mediaType: attachment.contentType!,1349 bytes,1350 }1351 const attachmentId = obs.attachments[0].id1352 const req: api.StoreAttachmentContentRequest = {1353 context,1354 observationId: obs.id,1355 attachmentId: obs.attachments[0].id,1356 content,1357 }1358 const attachmentPatch: AttachmentContentPatchAttrs = Object.freeze({ contentLocator: uniqid(), size: 887766 })1359 const afterStore = patchAttachment(obs, attachmentId, attachmentPatch) as Observation1360 obsRepo.findById(obs.id).resolves(obs)1361 store.saveContent(bytes, req.attachmentId, obs).resolves(attachmentPatch)1362 obsRepo.patchAttachment(Arg.all()).resolves(afterStore)1363 const res = await storeAttachmentContent(req)1364 expect(res.error).to.be.null1365 expect(res.success).to.deep.equal(api.exoObservationFor(afterStore))1366 store.received(1).saveContent(Arg.all())1367 store.received(1).saveContent(bytes, attachmentId, Arg.is(validObservation()))1368 store.received(1).saveContent(bytes, attachmentId, obs)1369 obsRepo.received(1).patchAttachment(Arg.all())1370 obsRepo.received(1).patchAttachment(Arg.is(validObservation()), attachmentId, attachmentPatch)1371 obsRepo.received(1).patchAttachment(Arg.is(equalToObservationIgnoringDates(obs)), attachmentId, attachmentPatch)1372 obsRepo.didNotReceive().save(Arg.all())1373 })1374 it('saves previously staged content to the attachment store', async function() {1375 const attachment = obs.attachments[0]1376 const stagedRef = new StagedAttachmentContentRef(uniqid())1377 const content: api.ExoIncomingAttachmentContent = {1378 name: attachment.name!,1379 mediaType: attachment.contentType!,1380 bytes: stagedRef,1381 }1382 const attachmentId = obs.attachments[0].id1383 const req: api.StoreAttachmentContentRequest = {1384 context,1385 observationId: obs.id,1386 attachmentId: obs.attachments[0].id,1387 content,1388 }1389 const attachmentPatch: AttachmentContentPatchAttrs = { contentLocator: uniqid(), size: 223344 }1390 const afterStore = patchAttachment(obs, attachmentId, attachmentPatch) as Observation1391 obsRepo.findById(obs.id).resolves(obs)1392 store.saveContent(stagedRef, req.attachmentId, obs).resolves(attachmentPatch)1393 obsRepo.patchAttachment(Arg.all()).resolves(afterStore)1394 const res = await storeAttachmentContent(req)1395 expect(res.error).to.be.null1396 expect(res.success).to.deep.equal(api.exoObservationFor(afterStore))1397 store.received(1).saveContent(Arg.all())1398 store.received(1).saveContent(stagedRef, attachmentId, Arg.is(validObservation()))1399 store.received(1).saveContent(stagedRef, attachmentId, obs)1400 obsRepo.received(1).patchAttachment(Arg.all())1401 obsRepo.received(1).patchAttachment(Arg.is(validObservation()), attachmentId, attachmentPatch)1402 obsRepo.received(1).patchAttachment(obs, attachmentId, attachmentPatch)1403 obsRepo.didNotReceive().save(Arg.all())1404 })1405 it('does not save the observation if the attachment store did not return a patch', async function() {1406 const attachment = obs.attachments[0]1407 const bytesBuffer = Buffer.from('photo of something')1408 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1409 const content: api.ExoIncomingAttachmentContent = {1410 name: attachment.name!,1411 mediaType: attachment.contentType!,1412 bytes,1413 }1414 const attachmentId = attachment.id1415 const req: api.StoreAttachmentContentRequest = {1416 context,1417 observationId: obs.id,1418 attachmentId: attachment.id,1419 content,1420 }1421 obsRepo.findById(obs.id).resolves(obs)1422 store.saveContent(bytes, req.attachmentId, obs).resolves(null)1423 const res = await storeAttachmentContent(req)1424 expect(res.error).to.be.null1425 expect(res.success).to.deep.equal(api.exoObservationFor(obs))1426 store.received(1).saveContent(Arg.all())1427 store.received(1).saveContent(bytes, attachmentId, Arg.is(validObservation()))1428 store.received(1).saveContent(bytes, attachmentId, obs)1429 obsRepo.didNotReceive().patchAttachment(Arg.all())1430 obsRepo.didNotReceive().save(Arg.all())1431 })1432 it('fails if storing the content failed', async function() {1433 const attachment = obs.attachments[0]1434 const bytesBuffer = Buffer.from('photo of something')1435 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1436 const content: api.ExoIncomingAttachmentContent = {1437 name: attachment.name!,1438 mediaType: attachment.contentType!,1439 bytes,1440 }1441 const req: api.StoreAttachmentContentRequest = {1442 context,1443 observationId: obs.id,1444 attachmentId: attachment.id,1445 content,1446 }1447 obsRepo.findById(obs.id).resolves(obs)1448 store.saveContent(Arg.all()).resolves(new AttachmentStoreError(AttachmentStoreErrorCode.StorageError, 'the imaginary storage failed'))1449 const res = await storeAttachmentContent(req)1450 const err = res.error as InfrastructureError1451 expect(res.success).to.be.null1452 expect(err).to.be.instanceOf(MageError)1453 expect(err.code).to.equal(ErrInfrastructure)1454 expect(err.message).to.contain('the imaginary storage failed')1455 store.received(1).saveContent(Arg.all())1456 obsRepo.didNotReceive().save(Arg.all())1457 })1458 it('fails if the observation does not exist', async function() {1459 const bytes = Sub.for<NodeJS.ReadableStream>()1460 const req: api.StoreAttachmentContentRequest = {1461 context,1462 observationId: uniqid(),1463 attachmentId: uniqid(),1464 content: { bytes, name: 'fail.jpg', mediaType: 'image/jpeg' },1465 }1466 obsRepo.findById(Arg.all()).resolves(null)1467 const res = await storeAttachmentContent(req)1468 const err = res.error as EntityNotFoundError1469 expect(res.success).to.be.null1470 expect(err).to.be.instanceOf(MageError)1471 expect(err.code).to.equal(ErrEntityNotFound)1472 expect(err.data.entityId).to.equal(req.observationId)1473 store.didNotReceive().saveContent(Arg.all())1474 obsRepo.didNotReceive().save(Arg.all())1475 })1476 it('fails if the attachment does not exist on the observation', async function() {1477 const attachment = obs.attachments[0]1478 const bytesBuffer = Buffer.from('photo of something')1479 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1480 const content: api.ExoIncomingAttachmentContent = {1481 name: attachment.name!,1482 mediaType: attachment.contentType!,1483 bytes,1484 }1485 const req: api.StoreAttachmentContentRequest = {1486 context,1487 observationId: obs.id,1488 attachmentId: 'wut',1489 content,1490 }1491 obsRepo.findById(obs.id).resolves(obs)1492 const res = await storeAttachmentContent(req)1493 const err = res.error as EntityNotFoundError1494 expect(res.success).to.be.null1495 expect(err).to.be.instanceOf(MageError)1496 expect(err.code).to.equal(ErrEntityNotFound)1497 expect(err.data.entityId).to.equal(req.attachmentId)1498 expect(err.data.entityType).to.equal('Attachment')1499 store.didNotReceive().saveContent(Arg.all())1500 obsRepo.didNotReceive().save(Arg.all())1501 })1502 it('fails if the request content name does not match', async function() {1503 const attachment = obs.attachments[0]1504 const bytesBuffer = Buffer.from('photo of something')1505 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1506 const content: api.ExoIncomingAttachmentContent = {1507 name: 'img_123.wut',1508 mediaType: attachment.contentType!,1509 bytes,1510 }1511 const req: api.StoreAttachmentContentRequest = {1512 context,1513 observationId: obs.id,1514 attachmentId: attachment.id,1515 content,1516 }1517 obsRepo.findById(obs.id).resolves(obs)1518 const res = await storeAttachmentContent(req)1519 const err = res.error as EntityNotFoundError1520 expect(res.success).to.be.null1521 expect(err).to.be.instanceOf(MageError)1522 expect(err.code).to.equal(ErrEntityNotFound)1523 expect(err.data.entityId).to.equal(req.attachmentId)1524 expect(err.data.entityType).to.equal('Attachment')1525 store.didNotReceive().saveContent(Arg.all())1526 obsRepo.didNotReceive().save(Arg.all())1527 })1528 it('fails if the request media type does not match', async function() {1529 const attachment = obs.attachments[0]1530 const bytesBuffer = Buffer.from('photo of something')1531 const bytes: NodeJS.ReadableStream = Readable.from(bytesBuffer)1532 const content: api.ExoIncomingAttachmentContent = {1533 name: attachment.name!,1534 mediaType: 'image/wut',1535 bytes,1536 }1537 const req: api.StoreAttachmentContentRequest = {1538 context,1539 observationId: obs.id,1540 attachmentId: attachment.id,1541 content,1542 }1543 obsRepo.findById(obs.id).resolves(obs)1544 const res = await storeAttachmentContent(req)1545 const err = res.error as EntityNotFoundError1546 expect(res.success).to.be.null1547 expect(err).to.be.instanceOf(MageError)1548 expect(err.code).to.equal(ErrEntityNotFound)1549 expect(err.data.entityId).to.equal(req.attachmentId)1550 expect(err.data.entityType).to.equal('Attachment')1551 store.didNotReceive().saveContent(Arg.all())1552 obsRepo.didNotReceive().save(Arg.all())1553 })1554 })1555 describe('reading attachment content', function() {1556 let obs: Observation1557 let store: SubstituteOf<AttachmentStore>1558 let readAttachmentContent: api.ReadAttachmentContent1559 beforeEach(function() {1560 store = Sub.for<AttachmentStore>()1561 const permittedUser = context.userId1562 permissions.ensureReadObservationPermission(Arg.all()).mimicks(async context => {1563 if (context.userId === permittedUser) {1564 return null1565 }1566 return permissionDenied('read observation', context.userId)1567 })1568 store = Sub.for<AttachmentStore>()1569 mageEvent = new MageEvent({1570 ...copyMageEventAttrs(mageEvent),1571 forms: [1572 {1573 id: 1,1574 name: 'Save Attachment Content',1575 archived: false,1576 color: '#12ab34',1577 fields: [1578 {1579 id: 1,1580 name: 'description',1581 title: 'Description',1582 type: FormFieldType.Text,1583 required: false,1584 },1585 {1586 id: 2,1587 name: 'attachments',1588 title: 'Attachments',1589 type: FormFieldType.Attachment,1590 required: false,1591 }1592 ],1593 userFields: []1594 }1595 ]1596 })1597 const baseObsAttrs: ObservationAttrs = {1598 id: uniqid(),1599 eventId: mageEvent.id,1600 createdAt: new Date(),1601 lastModified: new Date(),1602 type: 'Feature',1603 geometry: { type: 'Point', coordinates: [ 55, 66 ] },1604 properties: {1605 timestamp: new Date(),1606 forms: [1607 { id: 'formEntry1', formId: mageEvent.forms[0].id, description: `something interesting at ${new Date().toISOString()}` },1608 { id: 'formEntry2', formId: mageEvent.forms[0].id }1609 ]1610 },1611 states: [],1612 favoriteUserIds: [],1613 attachments: []1614 }1615 baseObsAttrs.attachments = [1616 {1617 id: uniqid(),1618 observationFormId: 'formEntry1',1619 fieldName: 'attachments',1620 name: 'store test 1.jpg',1621 contentType: 'image/jpeg',1622 size: 12345,1623 oriented: false,1624 thumbnails: [],1625 contentLocator: uniqid()1626 },1627 {1628 id: uniqid(),1629 observationFormId: 'formEntry2',1630 fieldName: 'attachments',1631 name: 'store test 2.jpg',1632 contentType: 'image/jpeg',1633 size: 23456,1634 oriented: false,1635 thumbnails: [],1636 }1637 ]1638 obs = Observation.evaluate(baseObsAttrs, mageEvent)1639 readAttachmentContent = ReadAttachmentContent(permissions, store)1640 expect(obs.validation.hasErrors).to.be.false1641 })1642 it('checks permission', async function() {1643 const forbiddenUser = uniqid()1644 const observationId = uniqid()1645 const attachmentId = uniqid()1646 context.userId = forbiddenUser1647 const req: api.ReadAttachmentContentRequest = {1648 context,1649 observationId,1650 attachmentId,1651 }1652 const res = await readAttachmentContent(req)1653 const denied = res.error as PermissionDeniedError1654 expect(res.success).to.be.null1655 expect(denied).to.be.instanceOf(MageError)1656 expect(denied.code).to.equal(ErrPermissionDenied)1657 expect(denied.data.permission).to.equal('read observation')1658 expect(denied.data.subject).to.equal(forbiddenUser)1659 obsRepo.didNotReceive().findById(Arg.all())1660 store.didNotReceive().readContent(Arg.all())1661 store.didNotReceive().readThumbnailContent(Arg.all())1662 })1663 it('fails if the observation does not exist', async function() {1664 const req: api.ReadAttachmentContentRequest = {1665 context,1666 observationId: uniqid(),1667 attachmentId: uniqid(),1668 }1669 obsRepo.findById(Arg.all()).resolves(null)1670 const res = await readAttachmentContent(req)1671 const err = res.error as EntityNotFoundError1672 expect(res.success).to.be.null1673 expect(err).to.be.instanceOf(MageError)1674 expect(err.code).to.equal(ErrEntityNotFound)1675 expect(err.data.entityId).to.equal(req.observationId)1676 expect(err.data.entityType).to.equal('Observation')1677 })1678 it('fails if the attachment does not exist on the observation', async function() {1679 const req: api.ReadAttachmentContentRequest = {1680 context,1681 observationId: obs.id,1682 attachmentId: uniqid(),1683 }1684 obsRepo.findById(Arg.all()).resolves(obs)1685 const res = await readAttachmentContent(req)1686 const err = res.error as EntityNotFoundError1687 expect(res.success).to.be.null1688 expect(err).to.be.instanceOf(MageError)1689 expect(err.code).to.equal(ErrEntityNotFound)1690 expect(err.data.entityId).to.equal(req.attachmentId)1691 expect(err.data.entityType).to.equal('Attachment')1692 })1693 it('fails if the content does not exist in the attachment store', async function() {1694 const req: api.ReadAttachmentContentRequest = {1695 context,1696 observationId: obs.id,1697 attachmentId: obs.attachments[1].id,1698 }1699 obsRepo.findById(Arg.all()).resolves(obs)1700 store.readContent(Arg.all()).resolves(null)1701 const res = await readAttachmentContent(req)1702 const err = res.error as EntityNotFoundError1703 expect(res.success).to.be.null1704 expect(err).to.be.instanceOf(MageError)1705 expect(err.code).to.equal(ErrEntityNotFound)1706 expect(err.data.entityId).to.equal(req.attachmentId)1707 expect(err.data.entityType).to.equal('AttachmentContent')1708 store.received(1).readContent(Arg.all())1709 store.received(1).readContent(req.attachmentId, obs, undefined)1710 })1711 it('returns the attachment meta-data and its full content stream', async function() {1712 const req: api.ReadAttachmentContentRequest = {1713 context,1714 observationId: obs.id,1715 attachmentId: obs.attachments[0].id,1716 }1717 const contentBytes = Buffer.from(Array.from({ length: 100 }).map(_ => uniqid()).join('+'))1718 obsRepo.findById(Arg.all()).resolves(obs)1719 store.readContent(Arg.all()).resolves(Readable.from(contentBytes))1720 const res = await readAttachmentContent(req)1721 const resContent = res.success as api.ExoAttachmentContent1722 const resStream = new BufferWriteable()1723 await util.promisify(pipeline)(resContent.bytes, resStream)1724 expect(res.error).to.be.null1725 expect(resContent.attachment).to.deep.equal(api.exoAttachmentFor(obs.attachments[0]))1726 expect(resStream.bytes).to.deep.equal(contentBytes)1727 store.received(1).readContent(Arg.all())1728 store.received(1).readContent(req.attachmentId, obs, undefined)1729 })1730 it('reads a range of the attachment content', async function() {1731 const req: api.ReadAttachmentContentRequest = {1732 context,1733 observationId: obs.id,1734 attachmentId: obs.attachments[0].id,1735 contentRange: { start: 120, end: 239 }1736 }1737 const contentBytes = Buffer.from(Array.from({ length: 100 }).map(_ => uniqid()).join('+'))1738 obsRepo.findById(Arg.all()).resolves(obs)1739 store.readContent(Arg.all()).resolves(Readable.from(contentBytes.slice(120, 240)))1740 const res = await readAttachmentContent(req)1741 const resContent = res.success as api.ExoAttachmentContent1742 const resStream = new BufferWriteable()1743 await util.promisify(pipeline)(resContent.bytes, resStream)1744 expect(res.error).to.be.null1745 expect(resContent.attachment).to.deep.equal(api.exoAttachmentFor(obs.attachments[0]))1746 expect(resContent.bytesRange).to.deep.equal({ start: 120, end: 239 })1747 expect(resStream.bytes).to.deep.equal(contentBytes.slice(120, 240))1748 store.received(1).readContent(Arg.all())1749 store.received(1).readContent(req.attachmentId, obs, Arg.deepEquals({ ...req.contentRange! }))1750 })1751 it('reads the thumbnail content for the requested minimum dimension', async function() {1752 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1753 minDimension: 200,1754 contentLocator: uniqid(),1755 contentType: 'image/jpeg',1756 name: `${obs.attachments[0].name}@200`,1757 size: 10001758 }) as Observation1759 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1760 minDimension: 400,1761 contentLocator: uniqid(),1762 contentType: 'image/jpeg',1763 name: `${obs.attachments[0].name}@400`,1764 size: 20001765 }) as Observation1766 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1767 minDimension: 600,1768 contentLocator: uniqid(),1769 contentType: 'image/jpeg',1770 name: `${obs.attachments[0].name}@600`,1771 size: 30001772 }) as Observation1773 const contentBytes = Buffer.from(Array.from({ length: obs.attachments[1].size! }).map((_, index) => index % 10).join())1774 const req: api.ReadAttachmentContentRequest = {1775 context,1776 observationId: obs.id,1777 attachmentId: obs.attachments[0].id,1778 minDimension: 2601779 }1780 obsRepo.findById(Arg.all()).resolves(obs)1781 store.readThumbnailContent(Arg.all()).resolves(Readable.from(contentBytes))1782 const res = await readAttachmentContent(req)1783 const resContent = res.success as api.ExoAttachmentContent1784 const resStream = new BufferWriteable()1785 await util.promisify(pipeline)(resContent.bytes, resStream)1786 expect(res.error).to.be.null1787 expect(resContent.attachment).to.deep.equal(api.exoAttachmentForThumbnail(1, obs.attachments[0]))1788 expect(resContent.bytesRange).to.be.undefined1789 expect(resStream.bytes).to.deep.equal(contentBytes)1790 store.didNotReceive().readContent(Arg.all())1791 store.received(1).readThumbnailContent(Arg.all())1792 store.received(1).readThumbnailContent(400, req.attachmentId, obs)1793 })1794 it('reads the full content if the store does not have the thumbnail content', async function() {1795 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1796 minDimension: 200,1797 contentLocator: uniqid(),1798 contentType: 'image/jpeg',1799 name: `${obs.attachments[0].name}@200`,1800 size: 10001801 }) as Observation1802 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1803 minDimension: 400,1804 contentLocator: uniqid(),1805 contentType: 'image/jpeg',1806 name: `${obs.attachments[0].name}@400`,1807 size: 20001808 }) as Observation1809 obs = putAttachmentThumbnailForMinDimension(obs, obs.attachments[0].id, {1810 minDimension: 600,1811 contentLocator: uniqid(),1812 contentType: 'image/jpeg',1813 name: `${obs.attachments[0].name}@600`,1814 size: 30001815 }) as Observation1816 const contentBytes = Buffer.from(Array.from({ length: obs.attachments[1].size! }).map((_, index) => index % 10).join())1817 const req: api.ReadAttachmentContentRequest = {1818 context,1819 observationId: obs.id,1820 attachmentId: obs.attachments[0].id,1821 minDimension: 2601822 }1823 obsRepo.findById(Arg.all()).resolves(obs)1824 store.readThumbnailContent(Arg.all()).resolves(null)1825 store.readContent(Arg.all()).resolves(Readable.from(contentBytes))1826 const res = await readAttachmentContent(req)1827 const resContent = res.success as api.ExoAttachmentContent1828 const resStream = new BufferWriteable()1829 await util.promisify(pipeline)(resContent.bytes, resStream)1830 expect(res.error).to.be.null1831 expect(resContent.attachment).to.deep.equal(api.exoAttachmentFor(obs.attachments[0]))1832 expect(resContent.bytesRange).to.be.undefined1833 expect(resStream.bytes).to.deep.equal(contentBytes)1834 store.received(1).readThumbnailContent(Arg.all())1835 store.received(1).readThumbnailContent(400, req.attachmentId, obs)1836 store.received(1).readContent(Arg.all())1837 store.received(1).readContent(req.attachmentId, obs, undefined)1838 })1839 })1840 describe('handling removed attachments', function() {1841 class DeleteAttachmentContent {1842 readonly promise: Promise<any>1843 private resolve: ((result: any) => any) | undefined = undefined1844 private reject: ((err?: any) => void) | undefined = undefined1845 constructor() {1846 this.promise = new Promise((resolve, reject) => {1847 this.resolve = resolve1848 this.reject = reject1849 })1850 }1851 resolvePromise(): Promise<any> {1852 this.resolve!(null)1853 return this.promise1854 }1855 rejectPromise(err: Error): Promise<any> {1856 this.reject!(err)1857 return this.promise1858 }1859 }1860 let attachmentStore: SubstituteOf<AttachmentStore>1861 beforeEach(function() {1862 attachmentStore = Sub.for<AttachmentStore>()1863 mageEvent = new MageEvent({1864 ...copyMageEventAttrs(mageEvent),1865 forms: [1866 {1867 id: 135,1868 name: 'Remove Attachments Test',1869 color: '#ab1234',1870 archived: false,1871 userFields: [],1872 fields: [1873 {1874 id: 1,1875 name: 'attachments',1876 title: 'Remove Attachments',1877 required: false,1878 type: FormFieldType.Attachment,1879 }1880 ]1881 }1882 ]1883 })1884 })1885 it('schedules a task to delete attachment content upon a removed attachments event', async function() {1886 let observation = Observation.evaluate({1887 id: uniqid(),1888 eventId: mageEvent.id,1889 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),1890 lastModified: new Date(),1891 type: 'Feature',1892 geometry: { type: 'Point', coordinates: [ 15, 27 ] },1893 properties: {1894 timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),1895 forms: [1896 { id: uniqid(), formId: 135 }1897 ]1898 },1899 states: [],1900 favoriteUserIds: [],1901 attachments: [],1902 }, mageEvent)1903 observation = addAttachment(observation, 'not_removed', 'attachments', observation.formEntries[0].id, {1904 oriented: false,1905 thumbnails: [],1906 name: 'not_removed.jpg',1907 contentLocator: uniqid()1908 }) as Observation1909 const removed1: Attachment = {1910 id: 'remove1',1911 observationFormId: observation.properties.forms[0].id,1912 fieldName: 'attachments',1913 oriented: false,1914 thumbnails: [],1915 contentLocator: uniqid()1916 }1917 const removed2: Attachment = {1918 id: 'removed2',1919 observationFormId: observation.properties.forms[0].id,1920 fieldName: 'attachments',1921 oriented: false,1922 thumbnails: [],1923 contentLocator: uniqid()1924 }1925 const deletes = [] as DeleteAttachmentContent[]1926 attachmentStore.deleteContent(Arg.all()).mimicks((att, obs) => {1927 const del = new DeleteAttachmentContent()1928 deletes.push(del)1929 return del.promise.then(_ => null)1930 })1931 const domainEvents = new EventEmitter()1932 registerDeleteRemovedAttachmentsHandler(domainEvents, attachmentStore)1933 domainEvents.emit(ObservationDomainEventType.AttachmentsRemoved, Object.freeze<ObservationEmitted<AttachmentsRemovedDomainEvent>>({1934 type: ObservationDomainEventType.AttachmentsRemoved,1935 observation,1936 removedAttachments: Object.freeze([ removed1, removed2 ])1937 }))1938 await new Promise(resolve => setTimeout(resolve, 0))1939 expect(deletes).to.have.length(2)1940 attachmentStore.received(2).deleteContent(Arg.all())1941 attachmentStore.received(1).deleteContent(removed1, observation)1942 attachmentStore.received(1).deleteContent(removed2, observation)1943 await Promise.all(deletes.map(x => x.resolvePromise()))1944 })1945 it('swallows rejections', async function() {1946 let observation = Observation.evaluate({1947 id: uniqid(),1948 eventId: mageEvent.id,1949 createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),1950 lastModified: new Date(),1951 type: 'Feature',1952 geometry: { type: 'Point', coordinates: [ 15, 27 ] },1953 properties: {1954 timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),1955 forms: [1956 { id: uniqid(), formId: 135 }1957 ]1958 },1959 states: [],1960 favoriteUserIds: [],1961 attachments: [],1962 }, mageEvent)1963 observation = addAttachment(observation, 'not_removed', 'attachments', observation.formEntries[0].id, {1964 oriented: false,1965 thumbnails: [],1966 name: 'not_removed.jpg',1967 contentLocator: uniqid()1968 }) as Observation1969 const removed1: Attachment = {1970 id: 'remove1',1971 observationFormId: observation.properties.forms[0].id,1972 fieldName: 'attachments',1973 oriented: false,1974 thumbnails: [],1975 contentLocator: uniqid()1976 }1977 const removed2: Attachment = {1978 id: 'removed2',1979 observationFormId: observation.properties.forms[0].id,1980 fieldName: 'attachments',1981 oriented: false,1982 thumbnails: [],1983 contentLocator: uniqid()1984 }1985 const deletes = [] as DeleteAttachmentContent[]1986 attachmentStore.deleteContent(Arg.all()).mimicks(async (att, obs) => {1987 deletes.push(new DeleteAttachmentContent())1988 return deletes[deletes.length - 1].promise1989 })1990 const domainEvents = new EventEmitter()1991 registerDeleteRemovedAttachmentsHandler(domainEvents, attachmentStore)1992 domainEvents.emit(ObservationDomainEventType.AttachmentsRemoved, Object.freeze<ObservationEmitted<AttachmentsRemovedDomainEvent>>({1993 type: ObservationDomainEventType.AttachmentsRemoved,1994 observation,1995 removedAttachments: Object.freeze([ removed1, removed2 ])1996 }))1997 await new Promise(resolve => setTimeout(resolve, 0))1998 expect(deletes).to.have.length(2)1999 attachmentStore.received(2).deleteContent(Arg.all())2000 attachmentStore.received(1).deleteContent(removed1, observation)2001 attachmentStore.received(1).deleteContent(removed2, observation)2002 deletes[0].rejectPromise(new Error('catch me')).catch(err => 'caught')2003 await deletes[1].resolvePromise()2004 })2005 })2006})2007function equalToObservationIgnoringDates(expected: ObservationAttrs, message?: string): (actual: ObservationAttrs) => boolean {2008 const expectedWithoutDates = _.omit(copyObservationAttrs(expected), 'createdAt', 'lastModified')2009 expectedWithoutDates.attachments.forEach(x => x.lastModified = new Date(0))2010 return actual => {2011 const actualWithoutDates = _.omit(copyObservationAttrs(actual), 'createdAt', 'lastModified')2012 actualWithoutDates.attachments.forEach(x => x.lastModified = new Date(0))2013 expect(actualWithoutDates).to.deep.equal(expectedWithoutDates, message)2014 return true2015 }2016}2017function validObservation(): (actual: Observation) => boolean {2018 return actual => !actual.validation.hasErrors2019}2020function omitUndefinedFrom<T extends object>(x: T): Partial<T> {2021 return _.omitBy(x, (value) => value === undefined)2022}2023function observationModFor(observation: ObservationAttrs): api.ExoObservationMod {2024 return _.omit(copyObservationAttrs(observation), 'eventId', 'createdAt', 'lastModified', 'states', 'attachments')...

Full Screen

Full Screen

flapyBirdScript.js

Source:flapyBirdScript.js Github

copy

Full Screen

1let container=document.getElementById("container");2let sky=document.getElementById("sky");3let ground=document.getElementById("ground");4let bird=document.getElementById("bird");5let birdBottom=400;6let birdLeft=200;7let jump=50;8let gameOver=false;9function gameStart(){10 11 12 let birdRef=setInterval(function(){ let g=1;13 birdBottom-=g;14 if(birdBottom<0){clearInterval(birdRef);}15 bird.style.bottom=birdBottom+"px";},10);16}17gameStart();18function jumpBird(){19if(birdBottom<=470) birdBottom+=jump;20bird.style.bottom=birdBottom+"px";21}22document.addEventListener("click",jumpBird);23function obstacle(){24 //let obsLeft=600;25 //let obsBottom=200;26 // let obs=document.createElement('div');27 // obs.style.left=600+"px";28 // obs.style.bottom=200+"px";29 // obs.classList.add("obstacle");30 //container.appendChild(obs);31 32 //let obsRef= setInterval(function(){33 // obsLeft-=obsSpeed;34 // obs.style.left=obsLeft+"px";},15);35 let createObs=setInterval(function(){36 let obsSpeed=2;37 let obsLeft=1300;38 let obsBottom=100*Math.random();39 let obs=document.createElement('div');40 obs.style.left=obsLeft+"px";41 obs.style.bottom=obsBottom+"px";42 if(!gameOver){ obs.classList.add("obstacle");43 container.appendChild(obs);44 let obsRef= setInterval(function(){let obsSpeed=2;45 if(gameOver) clearInterval(obsRef);46 obsLeft-=obsSpeed;47 obs.style.left=obsLeft+"px";48 49 if((obsBottom-200)<=birdBottom && (obsBottom+50)>=birdBottom && birdLeft<=(obsLeft+60) &&50 birdLeft>=(obsLeft)) {gameOver=true;clearInterval(obsRef);}51 },15);}52 else{53 clearInterval(createObs);54 // clearInterval(obsRef);55 } 56 },3000); 57}58obstacle();59function UpObstacle(){60 //let obsLeft=600;61 //let obsBottom=200;62 // let obs=document.createElement('div');63 // obs.style.left=600+"px";64 // obs.style.bottom=200+"px";65 // obs.classList.add("obstacle");66 //container.appendChild(obs);67 68 //let obsRef= setInterval(function(){let obsSpeed=2;69 // obsLeft-=obsSpeed;70 // obs.style.left=obsLeft+"px";},15);71 let createUpObs=setInterval(function(){72 let obsSpeed=2;73 let obsUpLeft=1300;74 let obsTop=(200*Math.random()+450);75 let obsUp=document.createElement('div');76 obsUp.style.left=obsUpLeft+"px";77 obsUp.style.bottom=(obsTop)+"px";78 if(!gameOver){ obsUp.classList.add("Upobstacle");79 container.appendChild(obsUp);80 let obsUpRef= setInterval(function(){let obsUpSpeed=2;81 if(gameOver) clearInterval(obsUpRef);82 obsUpLeft-=obsSpeed;83 obsUp.style.left=obsUpLeft+"px";84 85 if((obsTop-200)<=birdBottom && (obsTop+50)>=birdBottom && birdLeft<=(obsUpLeft+60) &&86 birdLeft>=(obsUpLeft)) {gameOver=true;clearInterval(obsUpRef);}87 },15);}88 else{89 clearInterval(createUpObs);90 // clearInterval(obsRef);91 } 92 },3000); 93}...

Full Screen

Full Screen

obs.service.test.js

Source:obs.service.test.js Github

copy

Full Screen

1/*jshint -W026, -W030 */2(function() {3 'use strict';4 describe('OpenMRS Obs Service unit tests', function(){5 beforeEach(function(){6 module('openmrs-ngresource.restServices');7 module('mock.data');8 });9 var httpBackend;10 var obsService;11 var settingsService;12 var v = 'custom:(uuid,obsDatetime,concept:(uuid,uuid),groupMembers,value:ref)';13 var mockData;14 beforeEach(inject(function ($injector) {15 httpBackend = $injector.get('$httpBackend');16 obsService = $injector.get('ObsResService');17 settingsService = $injector.get('OpenmrsSettings');18 mockData = $injector.get('mockData');19 }));20 afterEach(function() {21 httpBackend.verifyNoOutstandingExpectation();22 //httpBackend.verifyNoOutstandingRequest();23 });24 it('should have obs service defined', function () {25 expect(obsService).to.exist;26 });27 it('should make an api call to the obs resource when getObsByUuid is called with a uuid', function () {28 httpBackend.expectGET(settingsService.getCurrentRestUrlBase() + 'obs?v='+v).respond(mockData.getMockObs());29 obsService.getObsByUuid('passed-uuid', function (data){30 expect(data.uuid).to.equal('passed-uuid');31 });32 httpBackend.flush();33 });34 it('should make an api call to the obs resource when voidObs is called with a uuid', function () {35 httpBackend.expectDELETE(settingsService.getCurrentRestUrlBase() + 'obs?v='+v).respond(mockData.getMockObs());36 obsService.voidObs('passed-uuid', function (data){37 expect(data.uuid).to.equal('passed-uuid');38 });39 httpBackend.flush();40 });41 it('ObsService should have getObsByUuid method', function () {42 expect(obsService.getObsByUuid).to.be.an('function');43 });44 it('ObsService should have voidObs method', function () {45 expect(obsService.voidObs).to.be.an('function');46 });47 it('ObsService should have saveUpdateObs method', function () {48 expect(obsService.saveUpdateObs).to.be.an('function');49 });50 it('should Call errorCallback when getObsByUuid fails', function () {51 httpBackend.expectGET(settingsService.getCurrentRestUrlBase() + 'obs?v='+v).respond(500);52 obsService.getObsByUuid('passed-uuid', function (){},53 function(error){54 expect(error).to.equal('Error processing request',500);55 });56 httpBackend.flush();57 });58 });...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1var wpt = require('webpagetest');2var options = {3};4var webPageTest = new wpt(options);5webPageTest.runTest(testUrl, {6}, function(err, data) {7 if (err) {8 console.log(err);9 } else {10 console.log(data);11 }12});13var wpt = require('webpagetest');14var options = {15};16var webPageTest = new wpt(options);17webPageTest.runTest(testUrl, {18}).then(function(data) {19 console.log(data);20}).catch(function(err) {21 console.log(err);22});23var wpt = require('webpagetest');24var options = {25};26var webPageTest = new wpt(options);27webPageTest.runTest(testUrl, {28}).then(function(data) {29 console.log(data);30}).catch(function(err) {31 console.log(err);32});

Full Screen

Using AI Code Generation

copy

Full Screen

1var wpt = require('webpagetest');2var test = new wpt('www.webpagetest.org');3test.runTest(url, function(err, data) {4 if (err) {5 console.error(err);6 } else {7 console.log(data);8 }9});10var wpt = require('webpagetest');11var test = new wpt('www.webpagetest.org');12test.runTest(url, function(err, data) {13 if (err) {14 console.error(err);15 } else {16 console.log(data);17 }18});19var exec = require('child_process').exec;20var child = exec('node test.js', function (error, stdout, stderr) {21console.log('stdout: ' + stdout);22console.log('stderr: ' + stderr);23if (error !== null) {24 console.log('exec error: ' + error);25}26});27var exec = require('child_process').exec;28var child = exec('node test.js', function (error, stdout, stderr) {29console.log('stdout: ' + stdout);30console.log('stderr: ' + stderr);31if (error !== null) {32 console.log('exec error: ' + error);33}34});35var child1 = exec('node test1.js', function (error, stdout, stderr) {36console.log('stdout: ' + stdout);37console.log('stderr: ' + stderr);38if (error !== null) {39 console.log('exec error: ' + error);40}41});

Full Screen

Using AI Code Generation

copy

Full Screen

1var wpt = require('webpagetest');2var options = {3};4var test = new wpt('www.webpagetest.org', options.key);5var location = 'Dulles:Chrome';6var runs = 3;7var firstViewOnly = false;8var video = true;9var pollResults = 5;10var pollResultsInterval = 5;11var timeout = 1000;12test.runTest(url, {13}, function(err, data) {14 if (err) return console.error(err);15 console.log('Test started: ' + data.data.testId);16 console.log('View your test at: ' + data.data.userUrl);17});18var request = require('request');19var options = {20 qs: {21 }22};23request(options, function(err, res, body) {24 if (err) return console.error(err);25 console.log('Test started: ' + body.data.testId);26 console.log('View your test at: ' + body.data

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run wpt automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful