diff --git a/__tests__/fixtures/version-3/auth2-active.json b/__tests__/fixtures/version-3/auth2-active.json new file mode 100644 index 0000000000..cb459a6909 --- /dev/null +++ b/__tests__/fixtures/version-3/auth2-active.json @@ -0,0 +1,65 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://iiif.io/api/cookbook/recipe/0005-image-service/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "An actively authorized video" + ] + }, + "items": [ + { + "id": "https://auth.example.org/my-video1", + "type": "Canvas", + "label": { + "en": [ + "Canvas with a single IIIF video" + ] + }, + "height": 3024, + "width": 4032, + "items": [ + { + "id": "https://auth.example.org/my-video/page", + "type": "AnnotationPage", + "items": [ + { + "id": "https://auth.example.org/my-video/annotation", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://auth.example.org/my-video.mp4", + "type": "Video", + "service": [ + { + "id": "https://auth.example.org/probe/my-video", + "type": "AuthProbeService2", + "service" : [ + { + "id": "https://auth.example.org/login", + "type": "AuthAccessService2", + "profile": "active", + "label": { "en": [ "Login to Example Institution" ] }, + "service" : [ + { + "id": "https://auth.example.org/token", + "type": "AuthAccessTokenService2" + }, + { + "id": "https://auth.example.org/logout", + "type": "AuthLogoutService2", + "label": { "en": [ "Logout from Example Institution" ] } + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/__tests__/fixtures/version-3/auth2-kiosk.json b/__tests__/fixtures/version-3/auth2-kiosk.json new file mode 100644 index 0000000000..9187902447 --- /dev/null +++ b/__tests__/fixtures/version-3/auth2-kiosk.json @@ -0,0 +1,69 @@ +{ + "@context": "http://iiif.io/api/presentation/3/context.json", + "id": "https://iiif.io/api/cookbook/recipe/0005-image-service/manifest.json", + "type": "Manifest", + "label": { + "en": [ + "A kiosk-authorized image" + ] + }, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0005-image-service/canvas/p1", + "type": "Canvas", + "label": { + "en": [ + "Canvas with a single IIIF image" + ] + }, + "height": 3024, + "width": 4032, + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0005-image-service/page/p1/1", + "type": "AnnotationPage", + "items": [ + { + "id": "https://iiif.io/api/cookbook/recipe/0005-image-service/annotation/p0001-image", + "type": "Annotation", + "motivation": "painting", + "body": { + "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen/full/max/0/default.jpg", + "type": "Image", + "format": "image/jpeg", + "height": 3024, + "width": 4032, + "service": [ + { + "id": "https://auth.example.org/probe/918ecd18c2592080851777620de9bcb5-gottingen", + "type": "AuthProbeService2", + "service" : [ + { + "id": "https://auth.example.org/kiosk", + "type": "AuthAccessService2", + "profile": "kiosk", + "label": { "en": [ "Kiosk Login" ] }, + "service" : [ + { + "id": "https://auth.example.org/token/918ecd18c2592080851777620de9bcb5-gottingen", + "type": "AuthAccessTokenService2" + } + ] + } + ] + }, + { + "id": "https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen", + "profile": "level1", + "type": "ImageService3" + } + ] + }, + "target": "https://iiif.io/api/cookbook/recipe/0005-image-service/canvas/p1" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/__tests__/src/actions/probeResponse.test.js b/__tests__/src/actions/probeResponse.test.js new file mode 100644 index 0000000000..7c157ab35a --- /dev/null +++ b/__tests__/src/actions/probeResponse.test.js @@ -0,0 +1,52 @@ +import { JSONLDResource } from 'manifesto.js'; +import * as actions from '../../../src/state/actions'; +import ActionTypes from '../../../src/state/actions/action-types'; + +describe('probeResponse actions', () => { + describe('requestProbeResponse', () => { + it('requests a probeResponse from given a url', () => { + const id = 'abc123'; + const expectedAction = { + probeId: id, + type: ActionTypes.REQUEST_PROBE_RESPONSE, + }; + expect(actions.requestProbeResponse(id)).toEqual(expectedAction); + }); + }); + describe('receiveProbeResponse', () => { + it('receives a probeResponse', () => { + const id = 'abc123'; + const json = { + content: 'probe service request', + id, + }; + const expectedAction = { + probeId: id, + probeJson: json, + type: ActionTypes.RECEIVE_PROBE_RESPONSE, + }; + expect(actions.receiveProbeResponse(id, json)).toEqual(expectedAction); + }); + }); + describe('fetchProbeResponse', () => { + describe('success response', () => { + it('dispatches the REQUEST_PROBE_RESPONSE action', () => { + const resource = new JSONLDResource({ services: [{ id: 'someUrl', type: 'AuthProbeService2' }] }); + expect(actions.fetchProbeResponse({ resource })).toEqual({ + probeId: 'someUrl', + resource, + type: 'mirador/REQUEST_PROBE_RESPONSE', + }); + }); + }); + }); + describe('removeProbeResponse', () => { + it('removes an existing probeResponse', () => { + const expectedAction = { + probeId: 'foo', + type: ActionTypes.REMOVE_PROBE_RESPONSE, + }; + expect(actions.removeProbeResponse('foo')).toEqual(expectedAction); + }); + }); +}); diff --git a/__tests__/src/lib/MiradorCanvas.test.js b/__tests__/src/lib/MiradorCanvas.test.js index 5502c67746..26f4ce6b00 100644 --- a/__tests__/src/lib/MiradorCanvas.test.js +++ b/__tests__/src/lib/MiradorCanvas.test.js @@ -9,6 +9,8 @@ import fragmentFixtureV3 from '../../fixtures/version-3/hamilton.json'; import audioFixture from '../../fixtures/version-3/0002-mvm-audio.json'; import videoFixture from '../../fixtures/version-3/0015-start.json'; import videoWithAnnoCaptions from '../../fixtures/version-3/video_with_annotation_captions.json'; +import auth2WithImage from '../../fixtures/version-3/auth2-kiosk.json'; +import auth2WithVideo from '../../fixtures/version-3/auth2-active.json'; describe('MiradorCanvas', () => { let instance; @@ -133,4 +135,26 @@ describe('MiradorCanvas', () => { expect(instance.v3VttContent.length).toEqual(1); }); }); + describe('iiifImageResources', () => { + it('returns image resources', () => { + instance = new MiradorCanvas( + Utils.parseManifest(auth2WithImage).getSequences()[0].getCanvases()[0], + ); + expect(instance.iiifImageResources.length).toEqual(1); + }); + it('returns only image resources', () => { + instance = new MiradorCanvas( + Utils.parseManifest(auth2WithVideo).getSequences()[0].getCanvases()[0], + ); + expect(instance.iiifImageResources.length).toEqual(0); + }); + }); + describe('imageServiceIds', () => { + it('returns image service IDs', () => { + instance = new MiradorCanvas( + Utils.parseManifest(auth2WithImage).getSequences()[0].getCanvases()[0], + ); + expect(instance.imageServiceIds[0]).toEqual('https://iiif.io/api/image/3.0/example/reference/918ecd18c2592080851777620de9bcb5-gottingen'); + }); + }); }); diff --git a/__tests__/src/lib/getServices.test.js b/__tests__/src/lib/getServices.test.js new file mode 100644 index 0000000000..d7f4a15e99 --- /dev/null +++ b/__tests__/src/lib/getServices.test.js @@ -0,0 +1,90 @@ +import { v4 as uuid } from 'uuid'; +import { + anyAuthServices, getLogoutService, getProbeService, getTokenService, +} from '../../../src/lib/getServices'; + +/** + */ +function resourceFixtureWithService(props) { + return { + id: uuid(), + services: [ + { ...props }, + ], + type: 'Dataset', + }; +} + +/** + */ +function actualLogoutServiceId(resource) { + const service = getLogoutService(resource); + return service + && service.id; +} + +/** + */ +function actualTokenServiceId(resource) { + const service = getTokenService(resource); + return service + && service.id; +} + +/** + */ +function actualProbeServiceId(resource) { + const service = getProbeService(resource); + return service + && service.id; +} + +describe('anyAuthServices', () => { + it('returns a filtered list', () => { + const serviceId = uuid(); + const auth0 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/0/anyService' }); + expect(anyAuthServices(auth0).length).toEqual(1); + const auth1 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/1/anyService' }); + expect(anyAuthServices(auth1).length).toEqual(1); + const auth2 = resourceFixtureWithService({ id: serviceId, type: 'AuthAnyService2' }); + expect(anyAuthServices(auth2).length).toEqual(1); + const notAuthProfile = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/not-auth/1/anyService' }); + expect(anyAuthServices(notAuthProfile).length).toEqual(0); + const notAuthType = resourceFixtureWithService({ id: serviceId, type: 'NotAuthAnyService2' }); + expect(anyAuthServices(notAuthType).length).toEqual(0); + }); +}); + +describe('getLogoutService', () => { + it('returns a Service', () => { + const serviceId = uuid(); + const auth0 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/0/logout' }); + expect(actualLogoutServiceId(auth0)).toEqual(serviceId); + const auth1 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/1/logout' }); + expect(actualLogoutServiceId(auth1)).toEqual(serviceId); + const auth2 = resourceFixtureWithService({ id: serviceId, type: 'AuthLogoutService2' }); + expect(actualLogoutServiceId(auth2)).toEqual(serviceId); + }); +}); + +describe('getProbeService', () => { + it('returns a Service', () => { + const serviceId = uuid(); + const auth1 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/1/probe' }); + expect(actualProbeServiceId(auth1)).toEqual(serviceId); + const auth2 = resourceFixtureWithService({ id: serviceId, type: 'AuthProbeService2' }); + expect(actualProbeServiceId(auth2)).toEqual(serviceId); + }); +}); + +describe('getTokenService', () => { + it('returns a Service', () => { + const serviceId = uuid(); + const auth0 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/0/token' }); + expect(actualTokenServiceId(auth0)).toEqual(serviceId); + const auth1 = resourceFixtureWithService({ id: serviceId, profile: 'http://iiif.io/api/auth/1/token' }); + expect(actualTokenServiceId(auth1)).toEqual(serviceId); + const auth2 = resourceFixtureWithService({ id: serviceId, type: 'AuthAccessTokenService2' }); + expect(actualTokenServiceId(auth2)).toEqual(serviceId); + }); +}); diff --git a/__tests__/src/lib/typeFilters.test.js b/__tests__/src/lib/typeFilters.test.js new file mode 100644 index 0000000000..854712d785 --- /dev/null +++ b/__tests__/src/lib/typeFilters.test.js @@ -0,0 +1,52 @@ +import { JSONLDResource } from 'manifesto.js'; +import { v4 as uuid } from 'uuid'; +import { + filterByTypes, audioResourcesFrom, anyImageServices, hasImageService, + iiifImageResourcesFrom, textResourcesFrom, videoResourcesFrom, +} from '../../../src/lib/typeFilters'; + +/** + */ +function resourceFixtureForProps(props) { + return new JSONLDResource({ + id: uuid(), + ...props, + }); +} + +describe('filterByTypes', () => { + it('returns a resource of one type', () => { + const typeFixture = 'someType'; + const resourceFixture = resourceFixtureForProps({ '@type': typeFixture }); + expect(filterByTypes([resourceFixture], typeFixture)).toEqual([resourceFixture]); + }); + it('returns a resource of any given types', () => { + const typeFixture = 'someType'; + const resourceFixture = resourceFixtureForProps({ '@type': typeFixture }); + expect(filterByTypes([resourceFixture], ['anotherType', typeFixture])).toEqual([resourceFixture]); + }); +}); + +describe('audioResourcesFrom', () => { + it('returns a resource of audio type', () => { + const typeFixture = 'Audio'; + const resourceFixture = resourceFixtureForProps({ '@type': typeFixture }); + expect(audioResourcesFrom([resourceFixture])).toEqual([resourceFixture]); + }); +}); + +describe('videoResourcesFrom', () => { + it('returns a resource of audio type', () => { + const typeFixture = 'Video'; + const resourceFixture = resourceFixtureForProps({ '@type': typeFixture }); + expect(videoResourcesFrom([resourceFixture])).toEqual([resourceFixture]); + }); +}); + +describe('textResourcesFrom', () => { + it('returns a resource of audio type', () => { + const typeFixture = 'Document'; + const resourceFixture = resourceFixtureForProps({ '@type': typeFixture }); + expect(textResourcesFrom([resourceFixture])).toEqual([resourceFixture]); + }); +}); diff --git a/__tests__/src/reducers/accessTokens.test.js b/__tests__/src/reducers/accessTokens.test.js index 020c450f64..03b88c924f 100644 --- a/__tests__/src/reducers/accessTokens.test.js +++ b/__tests__/src/reducers/accessTokens.test.js @@ -36,7 +36,7 @@ describe('access tokens response reducer', () => { }, }); }); - it('should handle RECEIVE_INFO_RESPONSE_FAILURE', () => { + it('should handle RECEIVE_ACCESS_TOKEN_FAILURE', () => { expect(accessTokensReducer( { abc123: { @@ -73,4 +73,49 @@ describe('access tokens response reducer', () => { })).toEqual({}); }); }); + it('should handle RECEIVE_INFO_RESPONSE', () => { + expect(accessTokensReducer( + { + efg456: { + id: 'efg456', + isFetching: true, + }, + }, + { + infoId: 'abc123', + infoJson: { + '@type': 'sc:Manifest', + content: 'lots of canvases and metadata and such', + id: 'abc123', + }, + tokenServiceId: 'efg456', + type: ActionTypes.RECEIVE_INFO_RESPONSE, + }, + )).toMatchObject({ + efg456: { + id: 'efg456', + success: true, + }, + }); + }); + it('should handle RECEIVE_PROBE_RESPONSE', () => { + expect(accessTokensReducer( + { + efg456: { + id: 'efg456', + isFetching: true, + }, + }, + { + infoId: 'abc123', + tokenServiceId: 'efg456', + type: ActionTypes.RECEIVE_PROBE_RESPONSE, + }, + )).toMatchObject({ + efg456: { + id: 'efg456', + success: true, + }, + }); + }); }); diff --git a/__tests__/src/reducers/probeResponse.test.js b/__tests__/src/reducers/probeResponse.test.js new file mode 100644 index 0000000000..14dafe6a6b --- /dev/null +++ b/__tests__/src/reducers/probeResponse.test.js @@ -0,0 +1,95 @@ +import { probeResponsesReducer } from '../../../src/state/reducers/probeResponses'; +import ActionTypes from '../../../src/state/actions/action-types'; + +describe('probe response reducer', () => { + it('should handle REQUEST_PROBE_RESPONSE', () => { + expect(probeResponsesReducer({}, { + probeId: 'abc123', + type: ActionTypes.REQUEST_PROBE_RESPONSE, + })).toEqual({ + abc123: { + id: 'abc123', + isFetching: true, + }, + }); + }); + it('should handle RECEIVE_PROBE_RESPONSE', () => { + expect(probeResponsesReducer( + { + abc123: { + id: 'abc123', + isFetching: true, + }, + }, + { + probeId: 'abc123', + probeJson: { + '@type': 'sc:Manifest', + content: 'lots of canvases and metadata and such', + id: 'abc123', + }, + tokenServiceId: 'efg456', + type: ActionTypes.RECEIVE_PROBE_RESPONSE, + }, + )).toMatchObject({ + abc123: { + id: 'abc123', + isFetching: false, + json: {}, + tokenServiceId: 'efg456', + }, + }); + }); + it('should handle RECEIVE_PROBE_RESPONSE_FAILURE', () => { + expect(probeResponsesReducer( + { + abc123: { + id: 'abc123', + isFetching: true, + }, + }, + { + error: "This institution didn't enable CORS.", + probeId: 'abc123', + tokenServiceId: 'efg456', + type: ActionTypes.RECEIVE_PROBE_RESPONSE_FAILURE, + }, + )).toEqual({ + abc123: { + error: "This institution didn't enable CORS.", + id: 'abc123', + isFetching: false, + tokenServiceId: 'efg456', + }, + }); + }); + it('should handle REMOVE_PROBE_RESPONSE', () => { + expect(probeResponsesReducer( + { + abc123: { + id: 'abc123', + stuff: 'foo', + }, + def456: { + id: 'def456', + stuff: 'foo', + }, + }, + { + probeId: 'abc123', + type: ActionTypes.REMOVE_PROBE_RESPONSE, + }, + )).toEqual({ + def456: { + id: 'def456', + stuff: 'foo', + }, + }); + }); + it('should handle IMPORT_MIRADOR_STATE setting to clean state', () => { + expect(probeResponsesReducer({}, { + state: { probeResponses: { new: 'stuff' } }, + type: ActionTypes.IMPORT_MIRADOR_STATE, + })).toEqual({}); + }); +}); diff --git a/__tests__/src/selectors/auth.test.js b/__tests__/src/selectors/auth.test.js index 2071e99317..b06c3f740f 100644 --- a/__tests__/src/selectors/auth.test.js +++ b/__tests__/src/selectors/auth.test.js @@ -1,5 +1,6 @@ import manifestFixture001 from '../../fixtures/version-2/001.json'; import manifestFixture019 from '../../fixtures/version-2/019.json'; +import manifestFixtureAuth2ActiveVideo from '../../fixtures/version-3/auth2-active.json'; import settings from '../../../src/config/settings'; import { getAccessTokens, @@ -61,6 +62,9 @@ describe('selectCurrentAuthServices', () => { b: { json: manifestFixture019, }, + c: { + json: manifestFixtureAuth2ActiveVideo, + }, }, windows: { noCanvas: { @@ -84,6 +88,12 @@ describe('selectCurrentAuthServices', () => { 'https://purl.stanford.edu/fr426cg9537/iiif/canvas/fr426cg9537_1', ], }, + z: { + manifestId: 'c', + visibleCanvases: [ + 'https://auth.example.org/my-video1', + ], + }, }, }; @@ -93,6 +103,7 @@ describe('selectCurrentAuthServices', () => { it('returns the next auth service to try', () => { expect(selectCurrentAuthServices(state, { windowId: 'w' })[0].id).toEqual('external'); + expect(selectCurrentAuthServices(state, { windowId: 'z' })[0].id).toEqual('https://auth.example.org/login'); }); it('returns the service if the next auth service is interactive', () => { diff --git a/src/components/VideoViewer.js b/src/components/VideoViewer.js index e8a3d617df..bf9ef76779 100644 --- a/src/components/VideoViewer.js +++ b/src/components/VideoViewer.js @@ -9,15 +9,16 @@ export class VideoViewer extends Component { const { captions, classes, videoOptions, videoResources, } = this.props; + return (