diff --git a/package.json b/package.json index 804d6425f..61dc020b1 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ }, "jest": { "clearMocks": true, - "testURL": "https://www.example.com/" + "testMatch": ["**/test/unit/*.test.ts"] }, "engines": { "node": ">=10" diff --git a/test/unit/AccessTokenEvents.spec.js b/test/unit/AccessTokenEvents.spec.js deleted file mode 100644 index 43005a720..000000000 --- a/test/unit/AccessTokenEvents.spec.js +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { AccessTokenEvents } from '../../src/AccessTokenEvents'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -class StubTimer { - - constructor() { - this.cancelWasCalled = false; - } - - init(duration) { - this.duration = duration; - } - - cancel() { - this.cancelWasCalled = true; - } - - addHandler(){} - removeHandler(){} -} - -describe("AccessTokenEvents", function () { - - let subject; - let accessTokenExpiringTimer; - let accessTokenExpiredTimer; - - beforeEach(function () { - accessTokenExpiringTimer = new StubTimer(); - accessTokenExpiredTimer = new StubTimer(); - subject = new AccessTokenEvents({ - accessTokenExpiringTimer, accessTokenExpiredTimer - }); - }); - - describe("constructor", function () { - - it("should use default expiringNotificationTime", function () { - subject._accessTokenExpiringNotificationTime.should.equal(60); - }); - - }); - - describe("load", function () { - - it("should cancel existing timers", function () { - subject.load({}); - - accessTokenExpiringTimer.cancelWasCalled.should.be.true; - accessTokenExpiredTimer.cancelWasCalled.should.be.true; - }); - - it("should initialize timers", function () { - subject.load({ - access_token:"token", - expires_in : 70 - }); - - accessTokenExpiringTimer.duration.should.equal(10); - accessTokenExpiredTimer.duration.should.equal(71); - }); - - it("should immediately schedule expiring timer if expiration is soon", function () { - subject.load({ - access_token:"token", - expires_in : 10 - }); - - accessTokenExpiringTimer.duration.should.equal(1); - }); - - it("should not initialize expiring timer if already expired", function () { - subject.load({ - access_token:"token", - expires_in : 0 - }); - - assert.isUndefined(accessTokenExpiringTimer.duration); - }); - - it("should initialize expired timer if already expired", function () { - subject.load({ - access_token:"token", - expires_in : 0 - }); - - accessTokenExpiredTimer.duration.should.equal(1); - }); - - it("should not initialize timers if no access token", function () { - subject.load({ - expires_in : 70 - }); - - assert.isUndefined(accessTokenExpiringTimer.duration); - assert.isUndefined(accessTokenExpiredTimer.duration); - }); - - it("should not initialize timers if no expiration on access token", function () { - subject.load({ - access_token:"token" - }); - - assert.isUndefined(accessTokenExpiringTimer.duration); - assert.isUndefined(accessTokenExpiredTimer.duration); - }); - }); - - describe("unload", function () { - - it("should cancel timers", function () { - - subject.unload(); - - accessTokenExpiringTimer.cancelWasCalled.should.be.true; - accessTokenExpiredTimer.cancelWasCalled.should.be.true; - }); - - }); - -}); diff --git a/test/unit/AccessTokenEvents.test.ts b/test/unit/AccessTokenEvents.test.ts new file mode 100644 index 000000000..34ef378ab --- /dev/null +++ b/test/unit/AccessTokenEvents.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { AccessTokenEvents } from '../../src/AccessTokenEvents'; +import { Timer } from '../../src/Timer'; +import { User } from '../../src/User'; + +describe("AccessTokenEvents", () => { + + let subject: AccessTokenEvents; + let accessTokenExpiringTimer: StubTimer; + let accessTokenExpiredTimer: StubTimer; + + beforeEach(() => { + accessTokenExpiringTimer = new StubTimer("stub expiring timer"); + accessTokenExpiredTimer = new StubTimer("stub expired timer"); + subject = new AccessTokenEvents({ + accessTokenExpiringTimer, accessTokenExpiredTimer + }); + }); + + describe("constructor", () => { + + it("should use default expiringNotificationTime", () => { + // @ts-ignore + expect(subject._accessTokenExpiringNotificationTime).toEqual(60); + }); + + }); + + describe("load", () => { + + it("should cancel existing timers", () => { + // act + subject.load({} as User); + + // assert + expect(accessTokenExpiringTimer.cancelWasCalled).toEqual(true); + expect(accessTokenExpiredTimer.cancelWasCalled).toEqual(true); + }); + + it("should initialize timers", () => { + // act + subject.load({ + access_token:"token", + expires_in : 70 + } as User); + + // assert + expect(accessTokenExpiringTimer.duration).toEqual(10); + expect(accessTokenExpiredTimer.duration).toEqual(71); + }); + + it("should immediately schedule expiring timer if expiration is soon", () => { + // act + subject.load({ + access_token:"token", + expires_in : 10 + } as User); + + // assert + expect(accessTokenExpiringTimer.duration).toEqual(1); + }); + + it("should not initialize expiring timer if already expired", () => { + // act + subject.load({ + access_token:"token", + expires_in : 0 + } as User); + + // assert + expect(accessTokenExpiringTimer.duration).toEqual(undefined); + }); + + it("should initialize expired timer if already expired", () => { + // act + subject.load({ + access_token:"token", + expires_in : 0 + } as User); + + // assert + expect(accessTokenExpiredTimer.duration).toEqual(1); + }); + + it("should not initialize timers if no access token", () => { + // act + subject.load({ + expires_in : 70 + } as User); + + // assert + expect(accessTokenExpiringTimer.duration).toEqual(undefined); + expect(accessTokenExpiredTimer.duration).toEqual(undefined); + }); + + it("should not initialize timers if no expiration on access token", () => { + // act + subject.load({ + access_token:"token" + } as User); + + // assert + expect(accessTokenExpiringTimer.duration).toEqual(undefined); + expect(accessTokenExpiredTimer.duration).toEqual(undefined); + }); + }); + + describe("unload", () => { + + it("should cancel timers", () => { + // act + subject.unload(); + + // assert + expect(accessTokenExpiringTimer.cancelWasCalled).toEqual(true); + expect(accessTokenExpiredTimer.cancelWasCalled).toEqual(true); + }); + }); +}); + +class StubTimer extends Timer { + cancelWasCalled: boolean; + duration: any; + + constructor(name: string) { + super(name) + this.cancelWasCalled = false; + } + + init(duration: number) { + this.duration = duration; + } + + cancel() { + this.cancelWasCalled = true; + } + + addHandler() {} + removeHandler() {} +} diff --git a/test/unit/ErrorResponse.spec.js b/test/unit/ErrorResponse.spec.js deleted file mode 100644 index 3ae611b8e..000000000 --- a/test/unit/ErrorResponse.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { ErrorResponse } from '../../src/ErrorResponse'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; -let expect = chai.expect; - -describe("ErrorResponse", function() { - - describe("constructor", function() { - - it("should require a error param", function() { - try { - new ErrorResponse({}); - } - catch (e) { - e.message.should.contain('error'); - return; - } - assert.fail(); - }); - - it("should read error", function() { - let subject = new ErrorResponse({error:"foo"}); - subject.error.should.equal("foo"); - }); - - it("should read error_description", function() { - let subject = new ErrorResponse({error:"error", error_description:"foo"}); - subject.error_description.should.equal("foo"); - }); - - it("should read error_uri", function() { - let subject = new ErrorResponse({error:"error", error_uri:"foo"}); - subject.error_uri.should.equal("foo"); - }); - - it("should read state", function() { - let subject = new ErrorResponse({error:"error", state:"foo"}); - subject.state.should.equal("foo"); - }); - }); - - describe("message", function() { - it("should be description if set", function() { - let subject = new ErrorResponse({error:"error", error_description:"foo"}); - subject.message.should.equal("foo"); - }); - - it("should be error if description not set", function() { - let subject = new ErrorResponse({error:"error"}); - subject.message.should.equal("error"); - }); - - }); - - describe("name", function() { - it("should be class name", function() { - let subject = new ErrorResponse({error:"error"}); - subject.name.should.equal("ErrorResponse"); - }); - - }); - - describe("stack", function() { - - it("should be set", function() { - let subject = new ErrorResponse({error:"error"}); - subject.stack.should.be.ok; - }); - - }); -}); diff --git a/test/unit/ErrorResponse.test.ts b/test/unit/ErrorResponse.test.ts new file mode 100644 index 000000000..b7808ae42 --- /dev/null +++ b/test/unit/ErrorResponse.test.ts @@ -0,0 +1,94 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { ErrorResponse } from '../../src/ErrorResponse'; + +describe("ErrorResponse", () => { + + describe("constructor", () => { + + it("should require a error param", () => { + // act + try { + new ErrorResponse({}); + } + catch (e) { + expect(e.message).toContain("error"); + return; + } + + fail("should not come here"); + }); + + it("should read error", () => { + // act + let subject = new ErrorResponse({error:"foo"}); + + // assert + expect(subject.error).toEqual("foo"); + }); + + it("should read error_description", () => { + // act + let subject = new ErrorResponse({error:"error", error_description:"foo"}); + + // assert + expect(subject.error_description).toEqual("foo"); + }); + + it("should read error_uri", () => { + // act + let subject = new ErrorResponse({error:"error", error_uri:"foo"}); + + // assert + expect(subject.error_uri).toEqual("foo"); + }); + + it("should read state", () => { + // act + let subject = new ErrorResponse({error:"error", state:"foo"}); + + // assert + expect(subject.state).toEqual("foo"); + }); + + }); + + describe("message", () => { + it("should be description if set", () => { + // act + let subject = new ErrorResponse({error:"error", error_description:"foo"}); + + // assert + expect(subject.message).toEqual("foo"); + }); + + it("should be error if description not set", () => { + // act + let subject = new ErrorResponse({error:"error"}); + + // assert + expect(subject.message).toEqual("error"); + }); + }); + + describe("name", () => { + it("should be class name", () => { + // act + let subject = new ErrorResponse({error:"error"}); + + // assert + expect(subject.name).toEqual("ErrorResponse"); + }); + }); + + describe("stack", () => { + it("should be set", () => { + // act + let subject = new ErrorResponse({error:"error"}); + + // assert + expect(subject.stack).not.toBeNull(); + }); + }); +}); diff --git a/test/unit/Event.spec.js b/test/unit/Event.spec.js deleted file mode 100644 index 20bfa19a0..000000000 --- a/test/unit/Event.spec.js +++ /dev/null @@ -1,113 +0,0 @@ - -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Event } from '../../src/Event'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("Event", function () { - - let subject; - - beforeEach(function () { - subject = new Event("test name"); - }); - - describe("addHandler", function () { - - it("should allow callback to be invoked", function () { - var cb = function () { - cb.wasCalled = true; - }; - subject.addHandler(cb); - - subject.raise(); - - cb.wasCalled.should.be.true; - }); - - it("should allow multiple callbacks", function () { - var count = 0; - var cb = function () { - count++; - }; - subject.addHandler(cb); - subject.addHandler(cb); - subject.addHandler(cb); - subject.addHandler(cb); - - subject.raise(); - - count.should.equal(4); - }); - - }); - - describe("removeHandler", function () { - - it("should remove callback from being invoked", function () { - var cb = function () { - cb.wasCalled = true; - }; - cb.wasCalled = false; - - subject.addHandler(cb); - subject.removeHandler(cb); - subject.raise(); - - cb.wasCalled.should.be.false; - }); - - it("should remove individual callback", function () { - var count = 0; - var cb1 = function () { - count++; - }; - var cb2 = function () { - cb2.wasCalled = true; - }; - - subject.addHandler(cb1); - subject.addHandler(cb2); - subject.addHandler(cb1); - subject.removeHandler(cb1); - subject.removeHandler(cb1); - - subject.raise(); - - count.should.equal(0); - cb2.wasCalled.should.be.true; - }); - - }); - - describe("raise", function () { - - it("should pass params", function () { - var cb = function (a,b,c) { - a.should.equal(1); - b.should.equal(2); - c.should.equal(3); - }; - subject.addHandler(cb); - - subject.raise(1,2,3); - }); - - it("should allow passing no params", function () { - var cb = function (a,b,c) { - assert.isUndefined(a); - assert.isUndefined(b); - assert.isUndefined(c); - }; - subject.addHandler(cb); - - subject.raise(); - }); - - }); - -}); diff --git a/test/unit/Event.test.ts b/test/unit/Event.test.ts new file mode 100644 index 000000000..290b5f88c --- /dev/null +++ b/test/unit/Event.test.ts @@ -0,0 +1,122 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Event } from '../../src/Event'; + +describe("Event", () => { + + let subject: Event; + + beforeEach(() => { + subject = new Event("test name"); + }); + + describe("addHandler", () => { + + it("should allow callback to be invoked", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addHandler(cb); + subject.raise(); + + // assert + expect(cb).toBeCalled(); + }); + + it("should allow multiple callbacks", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addHandler(cb); + subject.addHandler(cb); + subject.addHandler(cb); + subject.addHandler(cb); + subject.raise(); + + // assert + expect(cb).toBeCalledTimes(4); + }); + }); + + describe("removeHandler", () => { + + it("should remove callback from being invoked", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addHandler(cb); + subject.removeHandler(cb); + subject.raise(); + + // assert + expect(cb).toBeCalledTimes(0); + }); + + it("should remove individual callback", () => { + // arrange + var cb1 = jest.fn(); + var cb2 = jest.fn(); + + // act + subject.addHandler(cb1); + subject.addHandler(cb2); + subject.addHandler(cb1); + subject.removeHandler(cb1); + subject.removeHandler(cb1); + + subject.raise(); + + // assert + expect(cb1).toBeCalledTimes(0); + expect(cb2).toBeCalledTimes(1); + }); + }); + + describe("raise", () => { + + it("should pass params", () => { + // arrange + let a: any = 10; + let b: any = 11; + let c: any = 12; + var cb = function (arg_a: any, arg_b: any, arg_c: any) { + a = arg_a; + b = arg_b; + c = arg_c; + }; + subject.addHandler(cb); + + // act + subject.raise(1, 2, 3); + + // assert + expect(a).toEqual(1); + expect(b).toEqual(2); + expect(c).toEqual(3); + }); + + it("should allow passing no params", () => { + // arrange + let a: any = 10; + let b: any = 11; + let c: any = 12; + var cb = function (arg_a: any, arg_b: any, arg_c: any) { + a = arg_a; + b = arg_b; + c = arg_c; + }; + subject.addHandler(cb); + + subject.raise(); + + // assert + expect(a).toEqual(undefined); + expect(b).toEqual(undefined); + expect(c).toEqual(undefined); + }); + }); +}); diff --git a/test/unit/JoseUtil.spec.js b/test/unit/JoseUtil.spec.js index 530fc6b43..4e88e9cff 100644 --- a/test/unit/JoseUtil.spec.js +++ b/test/unit/JoseUtil.spec.js @@ -1,6 +1,8 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +// TODO: port-ts - adapt when jsrsasign is compaitble with jest + import { JoseUtil as JoseUtilRsa } from '../../src/JoseUtilRsa'; import { JoseUtil as JoseUtilJsrsasign } from '../../src/JoseUtil'; import { Log } from '../../src/Log'; diff --git a/test/unit/JsonService.spec.js b/test/unit/JsonService.spec.js deleted file mode 100644 index 4c1d20cc1..000000000 --- a/test/unit/JsonService.spec.js +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { JsonService } from '../../src/JsonService'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("JsonService", function() { - let subject; - - let stubHttpRequest; - - beforeEach(function(){ - stubHttpRequest = new StubXMLHttpRequest(); - subject = new JsonService(null, ()=>stubHttpRequest); - }); - - describe("getJson", function() { - - it("should require a url parameter", function() { - try { - subject.getJson(); - } - catch(e) { - return; - } - - assert.fail(); - }); - - it("should return a promise", function() { - let p = subject.getJson("http://test"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should make GET request to url", function() { - let p = subject.getJson("http://test"); - stubHttpRequest.method.should.be.equal('GET'); - stubHttpRequest.url.should.be.equal('http://test'); - }); - - it("should set token as authorization header", function() { - let p = subject.getJson("http://test", "token"); - stubHttpRequest.headers.has('Authorization').should.be.true; - stubHttpRequest.headers.get('Authorization').should.be.equal('Bearer token'); - }); - - it("should fulfill promise when http response is 200", function(done) { - let p = subject.getJson("http://test"); - - p.then(result => { - result.should.not.be.undefined; - result.foo.should.be.equal(1); - result.bar.should.be.equal('test'); - - done(); - }); - - stubHttpRequest.status = 200; - stubHttpRequest.responseHeaders.set('Content-Type', 'application/json'); - stubHttpRequest.responseText = JSON.stringify({foo:1, bar:'test'}); - stubHttpRequest.onload(); - }); - - it("should reject promise when http response is not 200", function(done) { - let p = subject.getJson("http://test"); - - p.then(result => { - assert.fail(); - }, error => { - error.should.be.instanceof(Error); - error.message.should.contain('500'); - error.message.should.contain('server error'); - done(); - }); - - stubHttpRequest.status = 500; - stubHttpRequest.statusText = "server error"; - stubHttpRequest.onload(); - }); - - it("should reject promise when http response is error", function(done) { - let p = subject.getJson("http://test"); - - p.then(result => { - assert.fail(); - }, error => { - error.should.be.instanceof(Error); - error.message.should.be.equal('Network Error'); - done(); - }); - - stubHttpRequest.onerror(); - }); - - it("should reject promise when http response content type is not json", function(done) { - let p = subject.getJson("http://test"); - - p.then(result => { - assert.fail(); - }, error => { - error.should.be.instanceof(Error); - error.message.indexOf('text/html').should.be.above(-1); - done(); - }); - - stubHttpRequest.status = 200; - stubHttpRequest.responseHeaders.set('Content-Type', 'text/html'); - stubHttpRequest.responseText = JSON.stringify({foo:1, bar:'test'}); - stubHttpRequest.onload(); - }); - - it("should accept custom content type in response", function(done) { - subject = new JsonService(['foo/bar'], ()=>stubHttpRequest); - let p = subject.getJson("http://test"); - - p.then(result => { - result.foo.should.equal(1); - done(); - }); - - stubHttpRequest.status = 200; - stubHttpRequest.responseHeaders.set('Content-Type', 'foo/bar'); - stubHttpRequest.responseText = JSON.stringify({foo:1, bar:'test'}); - stubHttpRequest.onload(); - }); - }); -}); - -class StubXMLHttpRequest { - constructor() { - this.headers = new Map(); - this.responseHeaders = new Map(); - } - - open(method, url) { - this.method = method; - this.url = url; - } - - setRequestHeader(header, value){ - this.headers.set(header, value); - } - - getResponseHeader(name){ - return this.responseHeaders.get(name); - } - - send() { - } -} diff --git a/test/unit/JsonService.test.ts b/test/unit/JsonService.test.ts new file mode 100644 index 000000000..54f6b70d7 --- /dev/null +++ b/test/unit/JsonService.test.ts @@ -0,0 +1,163 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { JsonService } from '../../src/JsonService'; + +class XMLHttpRequestMock { + method: any; + headers: Map; + url: any; + + responseHeaders: Map; + responseText?: string; + status?: number; + statusText?: string; + + constructor() { + this.headers = new Map(); + this.responseHeaders = new Map(); + this.responseText = undefined; + this.status = undefined; + this.statusText = undefined; + + xmlHttpRequestMock = this; + } + + open(method: string, url: string) { + this.method = method; + this.url = url; + } + + setRequestHeader(header: string, value: string) { + this.headers.set(header, value); + } + + getResponseHeader(name: string) { + return this.responseHeaders.get(name); + } + + send(_body?: Document | BodyInit | null) { + } + onload(_ev: ProgressEvent) { + } + onerror(_ev: ProgressEvent) { + } +} +let xmlHttpRequestMock = new XMLHttpRequestMock(); + +describe("JsonService", () => { + let subject: JsonService; + + beforeEach(() =>{ + Log.logger = console; + Log.level = Log.NONE; + + subject = new JsonService(null, XMLHttpRequestMock as unknown as typeof XMLHttpRequest); + }); + + describe("getJson", () => { + + it("should return a promise", () => { + // act + let p = subject.getJson("http://test"); + + // assert + expect(p).toBeInstanceOf(Promise); + }); + + it("should make GET request to url", () => { + // act + subject.getJson("http://test"); + + // assert + expect(xmlHttpRequestMock.method).toEqual('GET'); + expect(xmlHttpRequestMock.url).toEqual('http://test'); + }); + + it("should set token as authorization header", () => { + // act + subject.getJson("http://test", "token"); + + // assert + expect(xmlHttpRequestMock.headers.has('Authorization')).toEqual(true); + expect(xmlHttpRequestMock.headers.get('Authorization')).toEqual('Bearer token'); + }); + + it("should fulfill promise when http response is 200", async () => { + // act + let p = subject.getJson("http://test"); + xmlHttpRequestMock.status = 200; + xmlHttpRequestMock.responseHeaders.set('Content-Type', 'application/json'); + xmlHttpRequestMock.responseText = JSON.stringify({foo:1, bar:'test'}); + xmlHttpRequestMock.onload(new ProgressEvent("dummy")); + let result = await p; + + // assert + expect(result).not.toBeUndefined(); + expect(result.foo).toEqual(1); + expect(result.bar).toEqual("test"); + }); + + it("should reject promise when http response is not 200", async () => { + // act + let p = subject.getJson("http://test"); + xmlHttpRequestMock.status = 500; + xmlHttpRequestMock.statusText = "server error"; + xmlHttpRequestMock.onload(new ProgressEvent("dummy")); + try { + await p; + fail("should not come here"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain('500'); + expect(error.message).toContain('server error'); + } + }); + + it("should reject promise when http response is error", async () => { + // act + let p = subject.getJson("http://test"); + xmlHttpRequestMock.onerror(new ProgressEvent("dummy")); + try { + await p; + fail("should not come here"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toEqual("Network Error"); + } + }); + + it("should reject promise when http response content type is not json", async () => { + // act + let p = subject.getJson("http://test"); + xmlHttpRequestMock.status = 200; + xmlHttpRequestMock.responseHeaders.set('Content-Type', 'text/html'); + xmlHttpRequestMock.responseText = JSON.stringify({foo:1, bar:'test'}); + xmlHttpRequestMock.onload(new ProgressEvent("dummy")); + try { + await p; + fail("should not come here"); + } catch (error) { + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain("Invalid response Content-Type: text/html"); + } + }); + + it("should accept custom content type in response", async () => { + // arrange + subject = new JsonService(['foo/bar'], XMLHttpRequestMock as unknown as typeof XMLHttpRequest); + + // act + let p = subject.getJson("http://test"); + xmlHttpRequestMock.status = 200; + xmlHttpRequestMock.responseHeaders.set('Content-Type', 'foo/bar'); + xmlHttpRequestMock.responseText = JSON.stringify({foo:1, bar:'test'}); + xmlHttpRequestMock.onload(new ProgressEvent("dummy")); + let result = await p; + + // assert + expect(result.foo).toEqual(1); + }); + }); +}); diff --git a/test/unit/Log.spec.js b/test/unit/Log.spec.js deleted file mode 100644 index d6c560a7f..000000000 --- a/test/unit/Log.spec.js +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("Log", function() { - - beforeEach(function() { - Log.reset(); - Log.level = Log.INFO; - }); - - describe("level", function() { - - it("should not log when set to NONE", function() { - let stub = new StubLog(); - Log.logger = stub; - Log.level = Log.NONE; - - Log.info("test info"); - Log.warn("test warn"); - Log.error("test error"); - - stub.infoWasCalled.should.be.false; - stub.warnWasCalled.should.be.false; - stub.errorWasCalled.should.be.false; - }); - - it("should not log info or warn for ERROR level", function() { - let stub = new StubLog(); - - Log.logger = stub; - Log.level = Log.ERROR; - - Log.info("test info"); - Log.warn("test warn"); - Log.error("test error"); - - stub.infoWasCalled.should.be.false; - stub.warnWasCalled.should.be.false; - stub.errorWasCalled.should.be.true; - }); - - it("should not log info for WARN level", function() { - let stub = new StubLog(); - - Log.logger =stub; - Log.level = Log.WARN; - - Log.info("test info"); - Log.warn("test warn"); - Log.error("test error"); - - stub.infoWasCalled.should.be.false; - stub.warnWasCalled.should.be.true; - stub.errorWasCalled.should.be.true; - }); - - it("should log to all for INFO level", function() { - let stub = new StubLog(); - - Log.logger = stub; - Log.level = Log.INFO; - - Log.info("test info"); - Log.warn("test warn"); - Log.error("test error"); - - stub.infoWasCalled.should.be.true; - stub.warnWasCalled.should.be.true; - stub.errorWasCalled.should.be.true; - }); - - }); - - describe("logger", function() { - - it("should use the logger specified", function() { - let stub = new StubLog(); - Log.logger = stub; - - Log.info("test info"); - Log.warn("test warn"); - Log.error("test error"); - - stub.infoParam.should.equal("test info"); - stub.warnParam.should.equal("test warn"); - stub.errorParam.should.equal("test error"); - }); - - }); - - describe("info", function() { - - it("should work with no config", function() { - Log.info("test"); - }); - - }); - - describe("warn", function() { - - it("should work with no config", function() { - Log.warn("test"); - }); - - }); - - describe("error", function() { - - it("should work with no config", function() { - Log.error("test"); - }); - - }); -}); - -class StubLog { - constructor(){ - this.infoWasCalled = false; - this.warnWasCalled = false; - this.errorWasCalled = false; - } - info(arg) { - this.infoParam = arg; - this.infoWasCalled = true; - } - warn(arg) { - this.warnParam = arg; - this.warnWasCalled = true; - } - error(arg) { - this.errorParam = arg; - this.errorWasCalled = true; - } -} diff --git a/test/unit/Log.test.ts b/test/unit/Log.test.ts new file mode 100644 index 000000000..656fa51b9 --- /dev/null +++ b/test/unit/Log.test.ts @@ -0,0 +1,157 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log, Logger } from '../../src/Log'; + +describe("Log", () => { + beforeEach(() => { + Log.reset(); + Log.level = Log.INFO; + }); + + describe("level", () => { + + it("should not log when set to NONE", () => { + // arrange + let stub = new StubLog(); + Log.logger = stub; + Log.level = Log.NONE; + + // act + Log.info("test info"); + Log.warn("test warn"); + Log.error("test error"); + + // assert + expect(stub.infoWasCalled).toEqual(false); + expect(stub.warnWasCalled).toEqual(false); + expect(stub.errorWasCalled).toEqual(false); + }); + + it("should not log info or warn for ERROR level", () => { + // arrange + let stub = new StubLog(); + Log.logger = stub; + Log.level = Log.ERROR; + + // act + Log.info("test info"); + Log.warn("test warn"); + Log.error("test error"); + + // assert + expect(stub.infoWasCalled).toEqual(false); + expect(stub.warnWasCalled).toEqual(false); + expect(stub.errorWasCalled).toEqual(true); + }); + + it("should not log info for WARN level", () => { + // arrange + let stub = new StubLog(); + Log.logger = stub; + Log.level = Log.WARN; + + // act + Log.info("test info"); + Log.warn("test warn"); + Log.error("test error"); + + // assert + expect(stub.infoWasCalled).toEqual(false); + expect(stub.warnWasCalled).toEqual(true); + expect(stub.errorWasCalled).toEqual(true); + }); + + it("should log to all for INFO level", () => { + // arrange + let stub = new StubLog(); + Log.logger = stub; + Log.level = Log.INFO; + + // act + Log.info("test info"); + Log.warn("test warn"); + Log.error("test error"); + + // assert + expect(stub.infoWasCalled).toEqual(true); + expect(stub.warnWasCalled).toEqual(true); + expect(stub.errorWasCalled).toEqual(true); + }); + }); + + describe("logger", () => { + + it("should use the logger specified", () => { + // arrange + let stub = new StubLog(); + Log.logger = stub; + + // act + Log.info("test info"); + Log.warn("test warn"); + Log.error("test error"); + + // assert + expect(stub.infoParam).toEqual("test info"); + expect(stub.warnParam).toEqual("test warn"); + expect(stub.errorParam).toEqual("test error"); + }); + }); + + describe("info", () => { + + it("should work with no config", () => { + Log.info("test"); + }); + }); + + describe("warn", () => { + + it("should work with no config", () => { + Log.warn("test"); + }); + + }); + + describe("error", () => { + + it("should work with no config", () => { + Log.error("test"); + }); + }); +}); + +class StubLog implements Logger { + debugWasCalled: boolean; + infoWasCalled: boolean; + warnWasCalled: boolean; + errorWasCalled: boolean; + debugParam: any; + infoParam: any; + warnParam: any; + errorParam: any; + + constructor() { + this.debugWasCalled = false; + this.infoWasCalled = false; + this.warnWasCalled = false; + this.errorWasCalled = false; + } + debug(arg: any) { + this.debugParam = arg; + this.debugWasCalled = true; + } + info(arg: any) { + this.infoParam = arg; + this.infoWasCalled = true; + } + warn(arg: any) { + this.warnParam = arg; + this.warnWasCalled = true; + } + error(arg: any) { + this.errorParam = arg; + this.errorWasCalled = true; + } +} diff --git a/test/unit/MetadataService.spec.js b/test/unit/MetadataService.spec.js deleted file mode 100644 index fc9c432f1..000000000 --- a/test/unit/MetadataService.spec.js +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { MetadataService } from '../../src/MetadataService'; - -import { StubJsonService } from './StubJsonService'; - -import chai from 'chai'; -import { Z_NO_COMPRESSION } from 'zlib'; -chai.should(); -let assert = chai.assert; - -describe("MetadataService", function() { - let subject; - let settings; - let stubJsonService; - - beforeEach(function() { - Log.logger = console; - Log.level = Log.NONE; - - settings = {}; - stubJsonService = new StubJsonService(); - subject = new MetadataService(settings, ()=>stubJsonService); - }); - - describe("constructor", function() { - - it("should require a settings param", function() { - try { - new MetadataService(); - } - catch (e) { - Log.debug(e.message); - e.message.should.contain('settings'); - return; - } - assert.fail(); - }); - - }); - - describe("getMetadata", function() { - - it("should return a promise", function() { - var p = subject.getMetadata(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should use metadata on settings", function(done) { - settings.metadata = "test"; - - let p = subject.getMetadata(); - - p.then(result => { - result.should.equal("test"); - done(); - }); - }); - - it("should require metadataUrl", function(done) { - delete settings.metadataUrl; - - let p = subject.getMetadata(); - - p.then(null, err => { - err.message.should.contain('metadataUrl'); - done(); - }); - }); - - it("should require metadataUrl", function(done) { - delete settings.metadataUrl; - - let p = subject.getMetadata(); - - p.then(null, err => { - err.message.should.contain('metadataUrl'); - done(); - }); - }); - - it("should use metadataUrl to make json call", function() { - settings.metadataUrl = "http://sts/metadata"; - stubJsonService.result = Promise.resolve('test'); - - subject.getMetadata(); - - stubJsonService.url.should.equal("http://sts/metadata"); - }); - - it("should return metadata from json call", function(done) { - settings.metadataUrl = "http://sts/metadata"; - stubJsonService.result = Promise.resolve({"test":"data"}); - - let p = subject.getMetadata(); - - p.then(result => { - result.should.deep.equal({"test":"data"}); - done(); - }); - }); - - it("should cache metadata from json call", function(done) { - settings.metadataUrl = "http://sts/metadata"; - stubJsonService.result = Promise.resolve({test:"value"}); - - let p = subject.getMetadata(); - - p.then(result => { - settings.metadata.should.deep.equal({test:"value"}); - done(); - }); - }); - - it("should merge metadata from seed", function(done) { - settings.metadataUrl = "http://sts/metadata"; - settings.metadataSeed = {test1:"one"}; - stubJsonService.result = Promise.resolve({test2:"two"}); - - let p = subject.getMetadata(); - - p.then(result => { - result.should.deep.equal({test1:"one", test2:"two"}); - settings.metadata.should.deep.equal({test1:"one", test2:"two"}); - done(); - }); - }); - - it("should fail if json call fails", function(done) { - settings.metadataUrl = "http://sts/metadata"; - stubJsonService.result = Promise.reject(new Error("test")); - - let p = subject.getMetadata(); - - p.then(null, err => { - err.message.should.contain("test"); - done(); - }); - }); - - }); - - describe("_getMetadataProperty", function() { - - it("should return a promise", function() { - var p = subject._getMetadataProperty(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should use metadata on settings", function(done) { - settings.metadata = { - foo: "test" - }; - - let p = subject._getMetadataProperty("foo"); - - p.then(result => { - result.should.equal("test"); - done(); - }); - }); - - it("should fail if no data on metadata", function(done) { - settings.metadata = { - }; - - let p = subject._getMetadataProperty("foo"); - - p.then(null, err => { - err.message.should.contain("foo"); - done(); - }); - }); - - it("should fail if json call to load metadata fails", function(done) { - - settings.metadataUrl = "http://sts/metadata"; - stubJsonService.result = Promise.reject(new Error("test")); - - let p = subject._getMetadataProperty("foo"); - - p.then(null, err => { - err.message.should.contain("test"); - done(); - }); - }); - - }); - - describe("getAuthorizationEndpoint", function() { - - it("should return value from metadata", function(done) { - settings.metadata = { - authorization_endpoint: "http://sts/authorize" - }; - - let p = subject.getAuthorizationEndpoint(); - - p.then(result => { - result.should.equal("http://sts/authorize"); - done(); - }); - }); - - }); - - describe("getUserInfoEndpoint", function() { - - it("should return value from", function(done) { - settings.metadata = { - userinfo_endpoint: "http://sts/userinfo" - }; - - let p = subject.getUserInfoEndpoint(); - - p.then(result => { - result.should.equal("http://sts/userinfo"); - done(); - }); - }); - - }); - - describe("getEndSessionEndpoint", function() { - - it("should return value from", function(done) { - settings.metadata = { - end_session_endpoint: "http://sts/signout" - }; - - let p = subject.getEndSessionEndpoint(); - - p.then(result => { - result.should.equal("http://sts/signout"); - done(); - }); - }); - - it("should support optional value", function(done) { - settings.metadata = { - }; - - let p = subject.getEndSessionEndpoint(); - - p.then(result => { - assert.isUndefined(result); - done(); - }); - }); - - }); - - describe("getCheckSessionIframe", function() { - - it("should return value from", function(done) { - settings.metadata = { - check_session_iframe: "http://sts/check_session" - }; - - let p = subject.getCheckSessionIframe(); - - p.then(result => { - result.should.equal("http://sts/check_session"); - done(); - }); - }); - - it("should support optional value", function(done) { - settings.metadata = { - }; - - let p = subject.getCheckSessionIframe(); - - p.then(result => { - assert.isUndefined(result); - done(); - }); - }); - - }); - - describe("getIssuer", function() { - - it("should return value from", function(done) { - settings.metadata = { - issuer: "http://sts" - }; - - let p = subject.getIssuer(); - - p.then(result => { - result.should.equal("http://sts"); - done(); - }); - }); - - }); - - describe("getSigningKeys", function() { - - it("should return a promise", function() { - var p = subject.getSigningKeys(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should use signingKeys on settings", function(done) { - settings.signingKeys = "test"; - - let p = subject.getSigningKeys(); - - p.then(result => { - result.should.equal("test"); - done(); - }); - }); - - it("should fail if metadata does not have jwks_uri", function(done) { - settings.metadata = "test"; - - let p = subject.getSigningKeys(); - - p.then(null, err => { - err.message.should.contain('jwks_uri'); - done(); - }); - }); - - it("should fail if keys missing on keyset from jwks_uri", function(done) { - settings.metadata = { - jwks_uri: "http://sts/metadata/keys" - }; - stubJsonService.result = Promise.resolve({}); - - let p = subject.getSigningKeys(); - - p.then(null, err => { - err.message.should.contain("keyset"); - done(); - }) - }); - - it("should make json call to jwks_uri", function(done) { - settings.metadata = { - jwks_uri: "http://sts/metadata/keys" - }; - stubJsonService.result = Promise.resolve({keys:[{ - use:'sig', - kid:"test" - }]}); - - let p = subject.getSigningKeys(); - - p.then(result => { - stubJsonService.url.should.equal("http://sts/metadata/keys"); - done(); - }); - }); - - it("should return keys from jwks_uri", function(done) { - - settings.metadata = { - jwks_uri: "http://sts/metadata/keys" - }; - stubJsonService.result = Promise.resolve({keys:[{ - use:'sig', - kid:"test" - }]}); - - let p = subject.getSigningKeys(); - - p.then(keys => { - keys.should.deep.equal([{ - use:'sig', - kid:"test" - }]); - done(); - }); - }); - - it("should cache keys in settings", function(done) { - settings.metadata = { - jwks_uri: "http://sts/metadata/keys" - }; - stubJsonService.result = Promise.resolve({keys:[{ - use:'sig', - kid:"test" - }]}); - - let p = subject.getSigningKeys(); - - p.then(keys => { - settings.signingKeys.should.deep.equal([{ - use:'sig', - kid:"test" - }]); - done(); - }) - }); - - }); -}); diff --git a/test/unit/MetadataService.test.ts b/test/unit/MetadataService.test.ts new file mode 100644 index 000000000..b85e862cb --- /dev/null +++ b/test/unit/MetadataService.test.ts @@ -0,0 +1,395 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { MetadataService } from '../../src/MetadataService'; + +import { StubJsonService } from './StubJsonService'; + +describe("MetadataService", () => { + let subject: MetadataService; + let stubJsonService: any; + let settings: any + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + + settings = {}; + stubJsonService = new StubJsonService(); + // @ts-ignore + subject = new MetadataService(settings, () => stubJsonService); + }); + + describe("getMetadata", () => { + + it("should return a promise", async () => { + // act + const p = subject.getMetadata(); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch {} + }); + + it("should use metadata on settings", async () => { + // arrange + settings.metadata = "test"; + + // act + const result = await subject.getMetadata(); + + // assert + expect(result).toEqual("test"); + }); + + it("should require metadataUrl", async () => { + // arrange + delete settings.metadataUrl; + + // act + try { + await subject.getMetadata(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('metadataUrl'); + } + }); + + it("should use metadataUrl to make json call", () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + stubJsonService.result = Promise.resolve('test'); + + // act + subject.getMetadata(); + + // assert + expect(stubJsonService.url).toEqual("http://sts/metadata"); + }); + + it("should return metadata from json call", async () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + stubJsonService.result = Promise.resolve({"test":"data"}); + + // act + const result = await subject.getMetadata(); + + // assert + expect(result).toEqual({"test":"data"}); + }); + + it("should cache metadata from json call", async () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + stubJsonService.result = Promise.resolve({test:"value"}); + + // act + await subject.getMetadata(); + + // assert + expect(settings.metadata).toEqual({test:"value"}); + }); + + it("should merge metadata from seed", async () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + settings.metadataSeed = {test1:"one"}; + stubJsonService.result = Promise.resolve({test2:"two"}); + + // act + const result = await subject.getMetadata(); + + // assert + expect(result).toEqual({test1:"one", test2:"two"}); + expect(settings.metadata).toEqual({test1:"one", test2:"two"}); + }); + + it("should fail if json call fails", async () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + stubJsonService.result = Promise.reject(new Error("test")); + + // act + try { + await subject.getMetadata(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("test"); + } + }); + }); + + describe("_getMetadataProperty", () => { + + it("should return a promise", async () => { + // act + var p = subject._getMetadataProperty("issuer"); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch {} + }); + + it("should use metadata on settings", async () => { + // arrange + settings.metadata = { + issuer: "test" + }; + + // act + const result = await subject._getMetadataProperty("issuer"); + + // assert + expect(result).toEqual("test"); + }); + + it("should fail if no data on metadata", async () => { + // arrange + settings.metadata = { + }; + + // act + try { + await subject._getMetadataProperty("issuer"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("issuer"); + } + }); + + it("should fail if json call to load metadata fails", async () => { + // arrange + settings.metadataUrl = "http://sts/metadata"; + stubJsonService.result = Promise.reject(new Error("test")); + + // act + try { + await subject._getMetadataProperty("issuer"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("test"); + } + }); + + }); + + describe("getAuthorizationEndpoint", () => { + + it("should return value from metadata", async () => { + // arrange + settings.metadata = { + authorization_endpoint: "http://sts/authorize" + }; + + // act + const result = await subject.getAuthorizationEndpoint(); + + // assert + expect(result).toEqual("http://sts/authorize"); + }); + + }); + + describe("getUserInfoEndpoint", () => { + + it("should return value from", async () => { + // arrange + settings.metadata = { + userinfo_endpoint: "http://sts/userinfo" + }; + + // act + const result = await subject.getUserInfoEndpoint(); + + // assert + expect(result).toEqual("http://sts/userinfo"); + }); + + }); + + describe("getEndSessionEndpoint", () => { + + it("should return value from", async () => { + // arrange + settings.metadata = { + end_session_endpoint: "http://sts/signout" + }; + + // act + const result = await subject.getEndSessionEndpoint(); + + // assert + expect(result).toEqual("http://sts/signout"); + }); + + it("should support optional value", async () => { + // arrange + settings.metadata = { + }; + + // act + const result = await subject.getEndSessionEndpoint(); + + // assert + expect(result).toBeUndefined(); + }); + + }); + + describe("getCheckSessionIframe", () => { + + it("should return value from", async () => { + // arrange + settings.metadata = { + check_session_iframe: "http://sts/check_session" + }; + + // act + const result = await subject.getCheckSessionIframe(); + + // assert + expect(result).toEqual("http://sts/check_session"); + }); + + it("should support optional value", async () => { + // arrange + settings.metadata = { + }; + + // act + const result = await subject.getCheckSessionIframe(); + + // assert + expect(result).toBeUndefined(); + }); + + }); + + describe("getIssuer", () => { + + it("should return value from", async () => { + // arrange + settings.metadata = { + issuer: "http://sts" + }; + + // act + const result = await subject.getIssuer(); + + // assert + expect(result).toEqual("http://sts"); + }); + + }); + + describe("getSigningKeys", () => { + + it("should return a promise", async () => { + // act + var p = subject.getSigningKeys(); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch {} + }); + + it("should use signingKeys on settings", async () => { + // arrange + settings.signingKeys = "test"; + + // act + const result = await subject.getSigningKeys(); + + // assert + expect(result).toEqual("test"); + }); + + it("should fail if metadata does not have jwks_uri", async () => { + // arrange + settings.metadata = "test"; + + // act + try { + await subject.getSigningKeys(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('jwks_uri'); + } + }); + + it("should fail if keys missing on keyset from jwks_uri", async () => { + // arrange + settings.metadata = { + jwks_uri: "http://sts/metadata/keys" + }; + stubJsonService.result = Promise.resolve({}); + + // act + try { + await subject.getSigningKeys(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('keyset'); + } + }); + + it("should make json call to jwks_uri", async () => { + // arrange + settings.metadata = { + jwks_uri: "http://sts/metadata/keys" + }; + stubJsonService.result = Promise.resolve({keys:[{ + use:'sig', + kid:"test" + }]}); + + // act + await subject.getSigningKeys(); + + // assert + expect(stubJsonService.url).toEqual("http://sts/metadata/keys"); + }); + + it("should return keys from jwks_uri", async () => { + // arrange + settings.metadata = { + jwks_uri: "http://sts/metadata/keys" + }; + const expectedKeys = [{ + use:'sig', + kid:"test" + }] + stubJsonService.result = Promise.resolve({ + keys: expectedKeys + }); + + // act + const result = await subject.getSigningKeys(); + + // assert + expect(result).toEqual(expectedKeys); + }); + + it("should cache keys in settings", async () => { + // arrange + settings.metadata = { + jwks_uri: "http://sts/metadata/keys" + }; + const expectedKeys = [{ + use:'sig', + kid:"test" + }] + stubJsonService.result = Promise.resolve({ + keys: expectedKeys + }); + + // act + await subject.getSigningKeys(); + + // assert + expect(settings.signingKeys).toEqual(expectedKeys); + }); + }); +}); diff --git a/test/unit/OidcClient.spec.js b/test/unit/OidcClient.spec.js deleted file mode 100644 index cdd7228df..000000000 --- a/test/unit/OidcClient.spec.js +++ /dev/null @@ -1,582 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Global } from '../../src/Global'; -import { OidcClient } from '../../src/OidcClient'; -import { SigninRequest } from '../../src/SigninRequest'; -import { SigninResponse } from '../../src/SigninResponse'; -import { ErrorResponse } from '../../src/ErrorResponse'; -import { SignoutRequest } from '../../src/SignoutRequest'; -import { SignoutResponse } from '../../src/SignoutResponse'; -import { State } from '../../src/State'; -import { SigninState } from '../../src/SigninState'; -import { OidcClientSettings } from '../../src/OidcClientSettings'; -import { MetadataService } from '../../src/MetadataService'; - -import { Log } from '../../src/Log'; - -import { StubMetadataService } from './StubMetadataService'; -import { StubStateStore } from './StubStateStore'; -import { StubResponseValidator } from './StubResponseValidator'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("OidcClient", function () { - let settings; - let subject; - let stubMetadataService; - let stubStore; - let stubValidator; - - beforeEach(function () { - - Global._testing(); - - Log.logger = console; - Log.level = Log.NONE; - - stubStore = new StubStateStore(); - stubValidator = new StubResponseValidator(); - stubMetadataService = new StubMetadataService(); - - settings = { - authority: 'authority', - client_id: 'client', - redirect_uri: "http://app", - post_logout_redirect_uri: "http://app", - stateStore: stubStore, - ResponseValidatorCtor: () => stubValidator, - MetadataServiceCtor: () => stubMetadataService - }; - subject = new OidcClient(settings); - }); - - describe("constructor", function () { - - it("should allow no settings", function () { - let subject = new OidcClientSettings(); - }); - - it("should expose settings", function () { - subject.settings.should.be.ok; - subject.settings.client_id.should.equal("client"); - }); - - it("should accept OidcClientSettings", function () { - let settings = new OidcClientSettings({ client_id: "client" }); - - let subject = new OidcClient(settings, { - stateStore: stubStore, - ResponseValidatorCtor: () => stubValidator, - MetadataServiceCtor: () => stubMetadataService - }); - - subject.settings.should.equal(settings); - }); - - }); - - describe("settings", function () { - - it("should be OidcClientSettings", function () { - subject.settings.should.be.instanceof(OidcClientSettings); - }); - - }); - - describe("metadataService", function () { - - it("should be MetadataService", function () { - subject.metadataService.should.be.equal(stubMetadataService); - }); - - }); - - describe("createSigninRequest", function () { - - it("should return a promise", function () { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - var p = subject.createSigninRequest(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should return SigninRequest", function (done) { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - - var p = subject.createSigninRequest(); - - p.then(request => { - request.should.be.instanceof(SigninRequest); - done(); - }); - }); - - it("should pass params to SigninRequest", function (done) { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - - var p = subject.createSigninRequest({ - data: 'foo', - response_type: 'bar', - response_mode: 'mode', - scope: 'baz', - redirect_uri: 'quux', - prompt: 'p', - display: 'd', - max_age: 'm', - ui_locales: 'u', - id_token_hint: 'ith', - login_hint: 'lh', - acr_values: 'av', - resource: 'res', - request: 'req', - request_uri: 'req_uri' - }); - - p.then(request => { - request.state.data.should.equal('foo'); - - var url = request.url; - url.should.contain("http://sts/authorize"); - url.should.contain("response_type=bar"); - url.should.contain("scope=baz"); - url.should.contain("redirect_uri=quux"); - url.should.contain("prompt=p"); - url.should.contain("display=d"); - url.should.contain("max_age=m"); - url.should.contain("ui_locales=u"); - url.should.contain("id_token_hint=ith"); - url.should.contain("login_hint=lh"); - url.should.contain("acr_values=av"); - url.should.contain("resource=res"); - url.should.contain("request=req"); - url.should.contain("request_uri=req_uri"); - url.should.contain("response_mode=mode"); - - done(); - }); - }); - - it("should pass state in place of data to SigninRequest", function (done) { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - - var p = subject.createSigninRequest({ - state: 'foo', - response_type: 'bar', - scope: 'baz', - redirect_uri: 'quux', - prompt: 'p', - display: 'd', - max_age: 'm', - ui_locales: 'u', - id_token_hint: 'ith', - login_hint: 'lh', - acr_values: 'av', - resource: 'res' - }); - - p.then(request => { - request.state.data.should.equal('foo'); - - var url = request.url; - url.should.contain("http://sts/authorize"); - url.should.contain("response_type=bar"); - url.should.contain("scope=baz"); - url.should.contain("redirect_uri=quux"); - url.should.contain("prompt=p"); - url.should.contain("display=d"); - url.should.contain("max_age=m"); - url.should.contain("ui_locales=u"); - url.should.contain("id_token_hint=ith"); - url.should.contain("login_hint=lh"); - url.should.contain("acr_values=av"); - url.should.contain("resource=res"); - - done(); - }); - }); - - it("should fail if hybrid code id_token requested", function (done) { - var p = subject.createSigninRequest({response_type:"code id_token"}); - p.then(null, err => { - err.message.should.contain("hybrid"); - done(); - }); - }); - - it("should fail if hybrid code token requested", function (done) { - var p = subject.createSigninRequest({response_type:"code token"}); - p.then(null, err => { - err.message.should.contain("hybrid"); - done(); - }); - }); - - it("should fail if hybrid code id_token token requested", function (done) { - var p = subject.createSigninRequest({response_type:"code id_token token"}); - p.then(null, err => { - err.message.should.contain("hybrid"); - done(); - }); - }); - - it("should fail if metadata fails", function (done) { - - stubMetadataService.getAuthorizationEndpointResult = Promise.reject(new Error("test")); - - var p = subject.createSigninRequest(); - - p.then(null, err => { - err.message.should.contain("test"); - done(); - }); - }); - - it("should fail if seting state into store fails", function (done) { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - stubStore.error = "foo"; - - var p = subject.createSigninRequest(); - - p.then(null, err => { - err.message.should.contain("foo"); - done(); - }); - }); - - it("should store state", function (done) { - stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); - - var p = subject.createSigninRequest(); - - p.then(request => { - stubStore.item.should.be.ok; - done(); - }); - }); - - }); - - describe("readSigninResponseState", function () { - - it("should return a promise", function () { - var p = subject.readSigninResponseState("state=state"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should fail if no state on response", function (done) { - stubStore.item = "state"; - subject.readSigninResponseState("").then(null, err => { - err.message.should.contain('state'); - done(); - }); - }); - - it("should fail if storage fails", function (done) { - stubStore.error = "fail"; - subject.readSigninResponseState("state=state").then(null, err => { - err.message.should.contain('fail'); - done(); - }); - }); - - it("should deserialize stored state and return state and response", function (done) { - stubStore.item = new SigninState({ id: '1', nonce: '2', authority:'authority', client_id:'client', request_type:'type' }).toStorageString(); - - subject.readSigninResponseState("state=1").then(({state, response}) => { - state.id.should.equal('1'); - state.nonce.should.equal('2'); - state.authority.should.equal('authority'); - state.client_id.should.equal('client'); - state.request_type.should.equal('type'); - response.state.should.equal('1'); - done(); - }); - }); - - }); - - describe("processSigninResponse", function () { - - it("should return a promise", function () { - var p = subject.processSigninResponse("state=state"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should fail if no state on response", function (done) { - stubStore.item = "state"; - subject.processSigninResponse("").then(null, err => { - err.message.should.contain('state'); - done(); - }); - }); - - it("should fail if storage fails", function (done) { - stubStore.error = "fail"; - subject.processSigninResponse("state=state").then(null, err => { - err.message.should.contain('fail'); - done(); - }); - }); - - it("should deserialize stored state and call validator", function (done) { - stubStore.item = new SigninState({ id: '1', nonce: '2', authority:'authority', client_id:'client' }).toStorageString(); - - subject.processSigninResponse("state=1").then(response => { - stubValidator.signinState.id.should.equal('1'); - stubValidator.signinState.nonce.should.equal('2'); - stubValidator.signinState.authority.should.equal('authority'); - stubValidator.signinState.client_id.should.equal('client'); - stubValidator.signinResponse.should.be.deep.equal(response); - done(); - }); - }); - - }); - - describe("createSignoutRequest", function () { - - it("should return a promise", function () { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - var p = subject.createSignoutRequest(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should return SignoutRequest", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - - var p = subject.createSignoutRequest(); - - p.then(request => { - request.should.be.instanceof(SignoutRequest); - done(); - }); - }); - - it("should pass state in place of data to SignoutRequest", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - - var p = subject.createSignoutRequest({ - state: 'foo', - post_logout_redirect_uri: "bar", - id_token_hint: "baz" - }); - - p.then(request => { - request.state.data.should.equal('foo'); - var url = request.url; - url.should.contain("http://sts/signout"); - url.should.contain("post_logout_redirect_uri=bar"); - url.should.contain("id_token_hint=baz"); - done(); - }); - }); - - it("should pass params to SignoutRequest", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - - var p = subject.createSignoutRequest({ - data: 'foo', - post_logout_redirect_uri: "bar", - id_token_hint: "baz" - }); - - p.then(request => { - request.state.data.should.equal('foo'); - var url = request.url; - url.should.contain("http://sts/signout"); - url.should.contain("post_logout_redirect_uri=bar"); - url.should.contain("id_token_hint=baz"); - done(); - }); - }); - - it("should fail if metadata fails", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.reject(new Error("test")); - - var p = subject.createSignoutRequest(); - - p.then(null, err => { - err.message.should.contain("test"); - done(); - }); - }); - - it("should fail if no signout endpoint on metadata", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve(undefined); - - var p = subject.createSignoutRequest(); - - p.catch(err => { - err.message.should.contain("no end session endpoint"); - done(); - }); - }); - - it("should store state", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - - var p = subject.createSignoutRequest({ - data:"foo", id_token_hint:'hint' - }); - - p.then(request => { - stubStore.item.should.be.ok; - done(); - }); - }); - - it("should not generate state if no data", function (done) { - stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); - - var p = subject.createSignoutRequest(); - - p.then(request => { - assert.isUndefined(stubStore.item); - done(); - }); - }); - }); - - describe("readSignoutResponseState", function () { - - it("should return a promise", function () { - var p = subject.readSignoutResponseState("state=state"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should return result if no state on response", function (done) { - subject.readSignoutResponseState("").then(({state, response}) => { - response.should.be.ok; - done(); - }); - }); - - it("should return error", function (done) { - subject.readSignoutResponseState("error=foo").then(null, err => { - err.error.should.equal("foo"); - done(); - }); - }); - - it("should fail if storage fails", function (done) { - stubStore.error = "fail"; - subject.readSignoutResponseState("state=state").then(null, err => { - err.message.should.contain('fail'); - done(); - }); - }); - - it("should deserialize stored state and return state and response", function (done) { - - stubStore.item = new State({ id: '1', request_type:'type' }).toStorageString(); - - subject.readSignoutResponseState("state=1").then(({state, response}) => { - state.id.should.equal('1'); - state.request_type.should.equal('type'); - response.state.should.be.equal('1'); - done(); - }); - }); - - it("should call validator with state even if error in response", function (done) { - - stubStore.item = new State({ id: '1', data:"bar" }).toStorageString(); - - subject.processSignoutResponse("state=1&error=foo").then(response => { - stubValidator.signoutState.id.should.equal('1'); - stubValidator.signoutResponse.should.be.deep.equal(response); - done(); - }); - }); - - }); - - describe("processSignoutResponse", function () { - - it("should return a promise", function () { - var p = subject.processSignoutResponse("state=state"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should return result if no state on response", function (done) { - subject.processSignoutResponse("").then(response => { - response.should.be.ok; - done(); - }); - }); - - it("should return error", function (done) { - subject.processSignoutResponse("error=foo").then(null, err => { - err.error.should.equal("foo"); - done(); - }); - }); - - it("should fail if storage fails", function (done) { - stubStore.error = "fail"; - subject.processSignoutResponse("state=state").then(null, err => { - err.message.should.contain('fail'); - done(); - }); - }); - - it("should deserialize stored state and call validator", function (done) { - - stubStore.item = new State({ id: '1' }).toStorageString(); - - subject.processSignoutResponse("state=1").then(response => { - stubValidator.signoutState.id.should.equal('1'); - stubValidator.signoutResponse.should.be.deep.equal(response); - done(); - }); - }); - - it("should call validator with state even if error in response", function (done) { - - stubStore.item = new State({ id: '1', data:"bar" }).toStorageString(); - - subject.processSignoutResponse("state=1&error=foo").then(response => { - stubValidator.signoutState.id.should.equal('1'); - stubValidator.signoutResponse.should.be.deep.equal(response); - done(); - }); - }); - - }); - - describe("clearStaleState", function () { - - it("should return a promise", function () { - var p = subject.clearStaleState(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should call State.clearStaleState", function () { - var oldState = State.clearStaleState; - - State.clearStaleState = function (store, age) { - State.clearStaleState.wasCalled = true; - State.clearStaleState.store = store; - State.clearStaleState.age = age; - }; - subject.clearStaleState(); - - State.clearStaleState.wasCalled.should.be.true; - State.clearStaleState.store.should.equal(subject._stateStore); - State.clearStaleState.age.should.equal(subject.settings.staleStateAge); - - State.clearStaleState = oldState; - }); - - }); - -}); diff --git a/test/unit/OidcClient.test.ts b/test/unit/OidcClient.test.ts new file mode 100644 index 000000000..a66e9d504 --- /dev/null +++ b/test/unit/OidcClient.test.ts @@ -0,0 +1,654 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { OidcClient } from '../../src/OidcClient'; +import { OidcClientSettingsStore } from '../../src/OidcClientSettings'; +import { SigninState } from '../../src/SigninState'; +import { State } from '../../src/State'; +import { SigninRequest } from '../../src/SigninRequest'; +import { SignoutRequest } from '../../src/SignoutRequest'; +import { SignoutResponse } from '../../src/SignoutResponse'; + +import { StubStateStore } from './StubStateStore'; +import { StubResponseValidator } from './StubResponseValidator'; +import { StubMetadataService } from './StubMetadataService'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("OidcClient", () => { + let stubStore: any; + let stubResponseValidator: StubResponseValidator; + let stubMetadataService: StubMetadataService; + let settings: any; + let subject: OidcClient; + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + + stubStore = new StubStateStore(); + stubResponseValidator = new StubResponseValidator(); + stubMetadataService = new StubMetadataService(); + + settings = { + authority: 'authority', + client_id: 'client', + redirect_uri: "http://app", + post_logout_redirect_uri: "http://app", + stateStore: stubStore, + ResponseValidatorCtor: () => stubResponseValidator, + MetadataServiceCtor: () => stubMetadataService + }; + subject = new OidcClient(settings); + }); + + describe("constructor", () => { + + it("should allow no settings", () => { + // act + new OidcClient(); + }); + + it("should expose settings", () => { + // assert + expect(subject.settings).not.toBeNull(); + expect(subject.settings.client_id).toEqual("client"); + }); + + it("should accept OidcClientSettings", () => { + // arrange + let settings = { + client_id: "client" + }; + + // act + new OidcClient(settings); + }); + }); + + describe("settings", () => { + + it("should be OidcClientSettings", () => { + // assert + expect(subject.settings).toBeInstanceOf(OidcClientSettingsStore); + }); + + }); + + describe("metadataService", () => { + + it("should be MetadataService", () => { + // assert + expect(subject.metadataService).toEqual(stubMetadataService); + }); + + }); + + describe("createSigninRequest", () => { + + it("should return a promise", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + + // act + var p = subject.createSigninRequest(); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should return SigninRequest", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + + // act + var request = await subject.createSigninRequest(); + + // assert + expect(request).toBeInstanceOf(SigninRequest); + }); + + it("should pass params to SigninRequest", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + + // act + var request = await subject.createSigninRequest({ + data: 'foo', + response_type: 'bar', + response_mode: 'mode', + scope: 'baz', + redirect_uri: 'quux', + prompt: 'p', + display: 'd', + max_age: 'm', + ui_locales: 'u', + id_token_hint: 'ith', + login_hint: 'lh', + acr_values: 'av', + resource: 'res', + request: 'req', + request_uri: 'req_uri' + }); + + // assert + expect(request.state.data).toEqual('foo'); + const url = request.url; + expect(url).toContain("http://sts/authorize"); + expect(url).toContain("response_type=bar"); + expect(url).toContain("scope=baz"); + expect(url).toContain("redirect_uri=quux"); + expect(url).toContain("prompt=p"); + expect(url).toContain("display=d"); + expect(url).toContain("max_age=m"); + expect(url).toContain("ui_locales=u"); + expect(url).toContain("id_token_hint=ith"); + expect(url).toContain("login_hint=lh"); + expect(url).toContain("acr_values=av"); + expect(url).toContain("resource=res"); + expect(url).toContain("request=req"); + expect(url).toContain("request_uri=req_uri"); + expect(url).toContain("response_mode=mode"); + }); + + it("should pass state in place of data to SigninRequest", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + + // act + var request = await subject.createSigninRequest({ + state: 'foo', + response_type: 'bar', + scope: 'baz', + redirect_uri: 'quux', + prompt: 'p', + display: 'd', + max_age: 'm', + ui_locales: 'u', + id_token_hint: 'ith', + login_hint: 'lh', + acr_values: 'av', + resource: 'res' + }); + + // assert + expect(request.state.data).toEqual('foo'); + const url = request.url; + expect(url).toContain("http://sts/authorize"); + expect(url).toContain("response_type=bar"); + expect(url).toContain("scope=baz"); + expect(url).toContain("redirect_uri=quux"); + expect(url).toContain("prompt=p"); + expect(url).toContain("display=d"); + expect(url).toContain("max_age=m"); + expect(url).toContain("ui_locales=u"); + expect(url).toContain("id_token_hint=ith"); + expect(url).toContain("login_hint=lh"); + expect(url).toContain("acr_values=av"); + expect(url).toContain("resource=res"); + }); + + it("should fail if hybrid code id_token requested", async () => { + // act + try { + await subject.createSigninRequest({response_type:"code id_token"}); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("hybrid"); + } + }); + + it("should fail if hybrid code token requested", async () => { + // act + try { + await subject.createSigninRequest({response_type:"code token"}); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("hybrid"); + } + }); + + it("should fail if hybrid code id_token token requested", async () => { + // act + try { + await subject.createSigninRequest({response_type:"code id_token token"}); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("hybrid"); + } + }); + + it("should fail if metadata fails", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.reject(new Error("test")); + + // act + try { + await subject.createSigninRequest(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("test"); + } + }); + + it("should fail if seting state into store fails", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + stubStore.error = "foo"; + + // act + try { + await subject.createSigninRequest(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("foo"); + } + }); + + it("should store state", async () => { + // arrange + stubMetadataService.getAuthorizationEndpointResult = Promise.resolve("http://sts/authorize"); + + // act + await subject.createSigninRequest(); + + // assert + expect(stubStore.item).toBeDefined(); + }); + }); + + describe("readSigninResponseState", () => { + + it("should return a promise", async () => { + // act + var p = subject.readSigninResponseState("state=state"); + + // asssert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should fail if no state on response", async () => { + // arrange + stubStore.item = "state"; + + // act + try { + await subject.readSigninResponseState(""); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('state'); + } + }); + + it("should fail if storage fails", async () => { + // arrange + stubStore.error = "fail"; + + // act + try { + await subject.readSigninResponseState("state=state") + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('fail'); + } + }); + + it("should deserialize stored state and return state and response", async () => { + // arrange + stubStore.item = new SigninState({ id: '1', nonce: '2', authority:'authority', client_id:'client', request_type:'type' }).toStorageString(); + + // act + let { state, response } = await subject.readSigninResponseState("state=1") + + // assert + expect(state.id).toEqual('1'); + expect(state.nonce).toEqual('2'); + expect(state.authority).toEqual('authority'); + expect(state.client_id).toEqual('client'); + expect(state.request_type).toEqual('type'); + expect(response.state).toEqual('1'); + }); + }); + + describe("processSigninResponse", () => { + + it("should return a promise", async () => { + // act + var p = subject.processSigninResponse("state=state"); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should fail if no state on response", async () => { + // arrange + stubStore.item = "state"; + + // act + try { + await subject.processSigninResponse(""); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('state'); + } + }); + + it("should fail if storage fails", async () => { + // arrange + stubStore.error = "fail"; + + // act + try { + await subject.processSigninResponse("state=state"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('fail'); + } + }); + + it("should deserialize stored state and call validator", async () => { + // arrange + stubStore.item = new SigninState({ id: '1', nonce: '2', authority:'authority', client_id:'client' }).toStorageString(); + + // act + let response = await subject.processSigninResponse("state=1"); + + // assert + expect(stubResponseValidator.signinState.id).toEqual('1'); + expect(stubResponseValidator.signinState.nonce).toEqual('2'); + expect(stubResponseValidator.signinState.authority).toEqual('authority'); + expect(stubResponseValidator.signinState.client_id).toEqual('client'); + expect(stubResponseValidator.signinResponse).toEqual(response); + }); + }); + + describe("createSignoutRequest", () => { + + it("should return a promise", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + var p = subject.createSignoutRequest(); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should return SignoutRequest", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + var request = await subject.createSignoutRequest(); + + // assert + expect(request).toBeInstanceOf(SignoutRequest); + }); + + it("should pass state in place of data to SignoutRequest", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + const request = await subject.createSignoutRequest({ + state: 'foo', + post_logout_redirect_uri: "bar", + id_token_hint: "baz" + }); + + // assert + expect(request.state).toBeDefined(); + expect(request.state!.data).toEqual('foo'); + const url = request.url; + expect(url).toContain("http://sts/signout"); + expect(url).toContain("post_logout_redirect_uri=bar"); + expect(url).toContain("id_token_hint=baz"); + }); + + it("should pass params to SignoutRequest", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + const request = await subject.createSignoutRequest({ + data: 'foo', + post_logout_redirect_uri: "bar", + id_token_hint: "baz" + }); + + // assert + expect(request.state).toBeDefined(); + expect(request.state!.data).toEqual('foo'); + const url = request.url; + expect(url).toContain("http://sts/signout"); + expect(url).toContain("post_logout_redirect_uri=bar"); + expect(url).toContain("id_token_hint=baz"); + }); + + it("should fail if metadata fails", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.reject(new Error("test")); + + // act + try { + await subject.createSignoutRequest(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("test"); + } + }); + + it("should fail if no signout endpoint on metadata", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve(undefined); + + // act + try { + await subject.createSignoutRequest(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("no end session endpoint"); + } + }); + + it("should store state", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + await subject.createSignoutRequest({ + data:"foo", id_token_hint:'hint' + }); + + // assert + expect(stubStore.item).toBeDefined(); + }); + + it("should not generate state if no data", async () => { + // arrange + stubMetadataService.getEndSessionEndpointResult = Promise.resolve("http://sts/signout"); + + // act + await subject.createSignoutRequest(); + + // assert + expect(stubStore.item).toBeUndefined(); + }); + }); + + describe("readSignoutResponseState", () => { + it("should return a promise", async () => { + // act + const p = subject.readSignoutResponseState("state=state"); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should return result if no state on response", async () => { + // act + const { response } = await subject.readSignoutResponseState(""); + + // assert + expect(response).toBeInstanceOf(SignoutResponse); + }); + + it("should return error", async () => { + // act + try { + await subject.readSignoutResponseState("error=foo"); + fail("should not come here"); + } catch (err) { + expect(err.error).toEqual("foo"); + } + }); + + it("should fail if storage fails", async () => { + // arrange + stubStore.error = "fail"; + + // act + try { + await subject.readSignoutResponseState("state=state"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("fail"); + } + }); + + it("should deserialize stored state and return state and response", async () => { + // arrange + stubStore.item = new State({ id: '1', request_type:'type' }).toStorageString(); + + // act + const { state, response } = await subject.readSignoutResponseState("state=1"); + + // assert + expect(state).toBeDefined(); + expect(state!.id).toEqual('1'); + expect(state!.request_type).toEqual('type'); + expect(response.state).toEqual('1'); + }); + + it("should call validator with state even if error in response", async () => { + // arrange + stubStore.item = new State({ id: '1', data:"bar" }).toStorageString(); + + // act + const response = await subject.processSignoutResponse("state=1&error=foo"); + + // assert + expect(stubResponseValidator.signoutState.id).toEqual('1'); + expect(stubResponseValidator.signoutResponse).toEqual(response); + }); + }); + + describe("processSignoutResponse", () => { + + it("should return a promise", async () => { + // act + var p = subject.processSignoutResponse("state=state"); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should return result if no state on response", async () => { + // act + const response = await subject.processSignoutResponse(""); + + // assert + expect(response).toBeInstanceOf(SignoutResponse); + }); + + it("should return error", async () => { + // act + try { + await subject.processSignoutResponse("error=foo"); + fail("should not come here"); + } catch (err) { + expect(err.error).toEqual("foo"); + } + }); + + it("should fail if storage fails", async () => { + // arrange + stubStore.error = "fail"; + + // act + try { + await subject.processSignoutResponse("state=state"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("fail"); + } + }); + + it("should deserialize stored state and call validator", async () => { + // arrange + stubStore.item = new State({ id: '1' }).toStorageString(); + + // act + const response = await subject.processSignoutResponse("state=1"); + + // assert + expect(stubResponseValidator.signoutState.id).toEqual('1'); + expect(stubResponseValidator.signoutResponse).toEqual(response); + }); + + it("should call validator with state even if error in response", async () => { + // arrange + stubStore.item = new State({ id: '1', data:"bar" }).toStorageString(); + + // act + const response = await subject.processSignoutResponse("state=1&error=foo"); + + // assert + expect(stubResponseValidator.signoutState.id).toEqual('1'); + expect(stubResponseValidator.signoutResponse).toEqual(response); + }); + }); + + describe("clearStaleState", () => { + + it("should return a promise", async () => { + // act + var p = subject.clearStaleState(); + + // assert + expect(p).toBeInstanceOf(Promise); + try { await p; } catch(_err) {} + }); + + it("should call State.clearStaleState", () => { + // arrange + var oldState = State.clearStaleState; + State.clearStaleState = jest.fn(); + + // act + subject.clearStaleState(); + + // assert + expect(State.clearStaleState).toBeCalled(); + State.clearStaleState = oldState; + }); + }); +}); diff --git a/test/unit/OidcClientSettings.spec.js b/test/unit/OidcClientSettings.spec.js deleted file mode 100644 index 1d9cd6d25..000000000 --- a/test/unit/OidcClientSettings.spec.js +++ /dev/null @@ -1,509 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { OidcClientSettings } from '../../src/OidcClientSettings'; -import { Global } from '../../src/Global'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("OidcClientSettings", function () { - - beforeEach(function () { - Global._testing(); - Log.logger = console; - Log.level = Log.NONE; - }); - - describe("client_id", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.client_id.should.equal("client"); - }); - - it("should not allow setting if previously set", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - authority: "http://sts" - }); - try { - subject.client_id = "diff"; - assert.fail(); - } - catch (e) { - e.message.should.contain("client_id"); - } - }); - - it("should allow setting if not previously set", function () { - let subject = new OidcClientSettings({ - }); - subject.client_id = "test"; - subject.client_id.should.equal("test"); - }); - }); - - describe("client_secret", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_secret: 'secret' - }); - subject.client_secret.should.equal("secret"); - }); - }); - - describe("response_type", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - response_type: "foo" - }); - subject.response_type.should.equal("foo"); - }); - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.response_type.should.equal("id_token"); - }); - - }); - - describe("scope", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - scope: "foo" - }); - subject.scope.should.equal("foo"); - }); - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.scope.should.equal("openid"); - }); - - }); - - describe("redirect_uri", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - redirect_uri: "foo" - }); - subject.redirect_uri.should.equal("foo"); - }); - - }); - - describe("post_logout_redirect_uri", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - post_logout_redirect_uri: "http://app/loggedout" - }); - subject.post_logout_redirect_uri.should.equal("http://app/loggedout"); - }); - }); - - describe("prompt", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - prompt: "foo" - }); - subject.prompt.should.equal("foo"); - }); - }); - - describe("display", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - display: "foo" - }); - subject.display.should.equal("foo"); - }); - }); - - describe("max_age", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - max_age: "foo" - }); - subject.max_age.should.equal("foo"); - }); - }); - - describe("ui_locales", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - ui_locales: "foo" - }); - subject.ui_locales.should.equal("foo"); - }); - }); - - describe("acr_values", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - acr_values: "foo" - }); - subject.acr_values.should.equal("foo"); - }); - }); - - describe("resource", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - resource: "foo" - }); - subject.resource.should.equal("foo"); - }); - }); - - describe("response_mode", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - response_mode: "foo" - }); - subject.response_mode.should.equal("foo"); - }); - }); - - describe("authority", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - authority: "http://sts" - }); - subject.authority.should.equal("http://sts"); - }); - - it("should not allow setting if previously set", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - authority: "http://sts" - }); - try { - subject.authority = "http://sts2"; - assert.fail(); - } - catch (e) { - e.message.should.contain("authority"); - } - }); - - it("should allow setting if not previously set", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.authority = "http://sts"; - subject.authority.should.equal("http://sts"); - }); - }); - - describe("metadataUrl", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - metadataUrl: "http://sts/metadata" - }); - subject.metadataUrl.should.equal("http://sts/metadata"); - }); - - it("should infer value from authority", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - authority: "http://sts" - }); - subject.metadataUrl.should.equal("http://sts/.well-known/openid-configuration"); - - subject = new OidcClientSettings({ - client_id: 'client', - authority: "http://sts/" - }); - subject.metadataUrl.should.equal("http://sts/.well-known/openid-configuration"); - }); - - }); - - describe("metadata", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - metadata: "test" - }); - subject.metadata.should.equal("test"); - }); - - it("should store value", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.metadata = "test"; - subject.metadata.should.equal("test"); - }); - - }); - - describe("signingKeys", function () { - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - signingKeys: "test" - }); - subject.signingKeys.should.equal("test"); - }); - - it("should store value", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.signingKeys = "test"; - subject.signingKeys.should.equal("test"); - }); - - }); - - describe("filterProtocolClaims", function () { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.filterProtocolClaims.should.equal(true); - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - filterProtocolClaims: true - }); - subject.filterProtocolClaims.should.equal(true); - - subject = new OidcClientSettings({ - client_id: 'client', - filterProtocolClaims: false - }); - subject.filterProtocolClaims.should.equal(false); - }); - }); - - describe("loadUserInfo", function () { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.loadUserInfo.should.equal(true); - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - loadUserInfo: true - }); - subject.loadUserInfo.should.equal(true); - - subject = new OidcClientSettings({ - client_id: 'client', - loadUserInfo: false - }); - subject.loadUserInfo.should.equal(false); - }); - }); - - describe("staleStateAge", function () { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.staleStateAge.should.equal(900); - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - staleStateAge: 100 - }); - subject.staleStateAge.should.equal(100); - }); - }); - - describe("clockSkew", function () { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.clockSkew.should.equal(5 * 60); // 5 mins - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - clockSkew: 10 - }); - subject.clockSkew.should.equal(10); - }); - }); - - describe("stateStore", function () { - - it("should return value from initial settings", function () { - let temp = {}; - let subject = new OidcClientSettings({ - client_id: 'client', - stateStore: temp - }); - subject.stateStore.should.equal(temp); - }); - }); - - describe("stateStore", function () { - - it("should return value from initial settings", function () { - let temp = {}; - let subject = new OidcClientSettings({ - client_id: 'client', - stateStore: temp - }); - subject.stateStore.should.equal(temp); - }); - }); - - describe("validator", function () { - - it("should return value from initial settings", function () { - - let temp = {}; - let subject = new OidcClientSettings({ - client_id: 'client', - ResponseValidatorCtor: function () { return temp } - }); - subject.validator.should.equal(temp); - }); - }); - - describe("metadataServiceCtor", function () { - - it("should return value from initial settings", function () { - - let temp = {}; - let subject = new OidcClientSettings({ - client_id: 'client', - MetadataServiceCtor: function () { return temp } - }); - subject.metadataService.should.equal(temp); - }); - }); - - describe("extraQueryParams", function() { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.extraQueryParams.should.deep.equal({}); - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - extraQueryParams: { - 'hd': 'domain.com' - } - }); - subject.extraQueryParams.should.deep.equal({ 'hd': 'domain.com' }); - }); - - it("should not set value from initial settings if not object, but set default value ({})", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - extraQueryParams: 123456 - }); - subject.extraQueryParams.should.deep.equal({}); - }); - - it("should set it if object", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.extraQueryParams = { 'hd': 'domain.com' }; - subject.extraQueryParams.should.deep.equal({ 'hd': 'domain.com' }); - }); - - it("should clear it if not object", function() { - let subject = new OidcClientSettings({ - client_id: 'client', - extraQueryParams: { - 'hd': 'domain.com', - } - }); - subject.extraQueryParams = undefined; - subject.extraQueryParams.should.deep.equal({}); - }); - }) - - describe("extraTokenParams", function() { - - it("should use default value", function () { - let subject = new OidcClientSettings({ - client_id: 'client' - }); - subject.extraTokenParams.should.deep.equal({}); - }); - - it("should return value from initial settings", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - extraTokenParams: { - 'resourceServer': 'abc' - } - }); - subject.extraTokenParams.should.deep.equal({ 'resourceServer': 'abc' }); - }); - - it("should not set value from initial settings if not object, but set default value ({})", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - extraTokenParams: 123456 - }); - subject.extraTokenParams.should.deep.equal({}); - }); - - it("should set it if object", function () { - let subject = new OidcClientSettings({ - client_id: 'client', - }); - subject.extraTokenParams = { 'resourceServer': 'abc' }; - subject.extraTokenParams.should.deep.equal({ 'resourceServer': 'abc' }); - }); - - it("should clear it if not object", function() { - let subject = new OidcClientSettings({ - client_id: 'client', - extraTokenParams: { - 'resourceServer': 'abc', - } - }); - subject.extraTokenParams = undefined; - subject.extraTokenParams.should.deep.equal({}); - }); - }) - -}); diff --git a/test/unit/OidcClientSettings.test.ts b/test/unit/OidcClientSettings.test.ts new file mode 100644 index 000000000..ad9ea6450 --- /dev/null +++ b/test/unit/OidcClientSettings.test.ts @@ -0,0 +1,681 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { MetadataService } from '../../src/MetadataService'; +import { OidcClientSettingsStore } from '../../src/OidcClientSettings'; +import { ResponseValidator } from '../../src/ResponseValidator'; +import { StateStore } from '../../src/StateStore'; +import { mocked } from 'ts-jest/utils'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("OidcClientSettings", () => { + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + }); + + describe("client_id", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.client_id).toEqual("client"); + }); + + it("should not allow setting if previously set", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client', + authority: "http://sts" + }); + + // act + try { + subject.client_id = "diff"; + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain("client_id"); + } + }); + + it("should allow setting if not previously set", () => { + // arrange + let subject = new OidcClientSettingsStore({ + }); + + // act + subject.client_id = "test"; + + // assert + expect(subject.client_id).toEqual("test"); + }); + }); + + describe("client_secret", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_secret: 'secret' + }); + + // assert + expect(subject.client_secret).toEqual("secret"); + }); + }); + + describe("response_type", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + response_type: "foo" + }); + + // assert + expect(subject.response_type).toEqual("foo"); + }); + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.response_type).toEqual("id_token"); + }); + + }); + + describe("scope", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + scope: "foo" + }); + + // assert + expect(subject.scope).toEqual("foo"); + }); + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.scope).toEqual("openid"); + }); + + }); + + describe("redirect_uri", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + redirect_uri: "foo" + }); + + // assert + expect(subject.redirect_uri).toEqual("foo"); + }); + + }); + + describe("post_logout_redirect_uri", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + post_logout_redirect_uri: "http://app/loggedout" + }); + + // assert + expect(subject.post_logout_redirect_uri).toEqual("http://app/loggedout"); + }); + }); + + describe("prompt", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + prompt: "foo" + }); + + // assert + expect(subject.prompt).toEqual("foo"); + }); + }); + + describe("display", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + display: "foo" + }); + + // assert + expect(subject.display).toEqual("foo"); + }); + }); + + describe("max_age", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + max_age: 22 + }); + + // assert + expect(subject.max_age).toEqual(22); + }); + }); + + describe("ui_locales", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + ui_locales: "foo" + }); + + // assert + expect(subject.ui_locales).toEqual("foo"); + }); + }); + + describe("acr_values", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + acr_values: "foo" + }); + + // assert + expect(subject.acr_values).toEqual("foo"); + }); + }); + + describe("resource", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + resource: "foo" + }); + + // assert + expect(subject.resource).toEqual("foo"); + }); + }); + + describe("response_mode", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + response_mode: "foo" + }); + + // assert + expect(subject.response_mode).toEqual("foo"); + }); + }); + + describe("authority", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + authority: "http://sts" + }); + + // assert + expect(subject.authority).toEqual("http://sts"); + }); + + it("should not allow setting if previously set", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client', + authority: "http://sts" + }); + + // act + try { + subject.authority = "http://sts2"; + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain("authority"); + } + }); + + it("should allow setting if not previously set", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // act + subject.authority = "http://sts"; + + // assert + expect(subject.authority).toEqual("http://sts"); + }); + }); + + describe("metadataUrl", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + metadataUrl: "http://sts/metadata" + }); + + // assert + expect(subject.metadataUrl).toEqual("http://sts/metadata"); + }); + + it("should infer value from authority", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + authority: "http://sts" + }); + + // assert + expect(subject.metadataUrl).toEqual("http://sts/.well-known/openid-configuration"); + }); + }); + + describe("metadata", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + metadata: { issuer: "test" } + }); + + // assert + expect(subject.metadata).toEqual({ issuer: "test" }); + }); + + it("should store value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + subject.metadata = { issuer: "test" }; + + // assert + expect(subject.metadata).toEqual({ issuer: "test" }); + }); + + }); + + describe("signingKeys", () => { + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + signingKeys: ["test"] + }); + + // assert + expect(subject.signingKeys).toEqual(["test"]); + }); + + it("should store value", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + + // act + subject.signingKeys = ["test"]; + + // assert + expect(subject.signingKeys).toEqual(["test"]); + }); + + }); + + describe("filterProtocolClaims", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + + // assert + expect(subject.filterProtocolClaims).toEqual(true); + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + filterProtocolClaims: true + }); + + // assert + expect(subject.filterProtocolClaims).toEqual(true); + + // act + subject = new OidcClientSettingsStore({ + client_id: 'client', + filterProtocolClaims: false + }); + + // assert + expect(subject.filterProtocolClaims).toEqual(false); + }); + }); + + describe("loadUserInfo", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + + // assert + expect(subject.loadUserInfo).toEqual(true); + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + loadUserInfo: true + }); + + // assert + expect(subject.loadUserInfo).toEqual(true); + + // act + subject = new OidcClientSettingsStore({ + client_id: 'client', + loadUserInfo: false + }); + + // assert + expect(subject.loadUserInfo).toEqual(false); + }); + }); + + describe("staleStateAge", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + + // assert + expect(subject.staleStateAge).toEqual(900); + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + staleStateAge: 100 + }); + + // assert + expect(subject.staleStateAge).toEqual(100); + }); + }); + + describe("clockSkew", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.clockSkew).toEqual(5 * 60); // 5 mins + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + clockSkew: 10 + }); + + // assert + expect(subject.clockSkew).toEqual(10); + }); + }); + + describe("stateStore", () => { + + it("should return value from initial settings", () => { + // arrange + let temp = {} as StateStore; + + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + stateStore: temp + }); + + // assert + expect(subject.stateStore).toEqual(temp); + }); + }); + + describe("stateStore", () => { + + it("should return value from initial settings", () => { + // arrange + let temp = {} as StateStore; + + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + stateStore: temp + }); + + // assert + expect(subject.stateStore).toEqual(temp); + }); + }); + + describe("validator", () => { + + it("should return value from initial settings", () => { + // arrange + const dummy = new OidcClientSettingsStore(); + const mock = mocked(new ResponseValidator(dummy)); + const settings: any = { + client_id: 'client', + ResponseValidatorCtor: () => mock + }; + + // act + let subject = new OidcClientSettingsStore(settings); + + // assert + expect(subject.validator).toEqual(mock); + }); + }); + + describe("metadataServiceCtor", () => { + + it("should return value from initial settings", () => { + // arrange + const dummy = new OidcClientSettingsStore(); + const mock = mocked(new MetadataService(dummy)); + const settings: any = { + client_id: 'client', + MetadataServiceCtor: () => mock + }; + + // act + let subject = new OidcClientSettingsStore(settings); + + // assert + expect(subject.metadataService).toEqual(mock); + }); + }); + + describe("extraQueryParams", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.extraQueryParams).toEqual({}); + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraQueryParams: { + 'hd': 'domain.com' + } + }); + + // assert + expect(subject.extraQueryParams).toEqual({ 'hd': 'domain.com' }); + }); + + it("should not set value from initial settings if not object, but set default value ({})", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraQueryParams: 123456 as unknown as Record + }); + + // assert + expect(subject.extraQueryParams).toEqual({}); + }); + + it("should set it if object", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + + // act + subject.extraQueryParams = { 'hd': 'domain.com' }; + + // assert + expect(subject.extraQueryParams).toEqual({ 'hd': 'domain.com' }); + }); + + it("should clear it if not object", () => { + // arrange + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraQueryParams: { + 'hd': 'domain.com', + } + }); + + // act + subject.extraQueryParams = undefined; + + // assert + expect(subject.extraQueryParams).toEqual({}); + }); + }) + + describe("extraTokenParams", () => { + + it("should use default value", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.extraTokenParams).toEqual({}); + }); + + it("should return value from initial settings", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraTokenParams: { + 'resourceServer': 'abc' + } + }); + + // assert + expect(subject.extraTokenParams).toEqual({ 'resourceServer': 'abc' }); + }); + + it("should not set value from initial settings if not object, but set default value ({})", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraTokenParams: 123456 as unknown as Record + }); + + // assert + expect(subject.extraTokenParams).toEqual({}); + }); + + it("should set it if object", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + }); + subject.extraTokenParams = { 'resourceServer': 'abc' }; + + // assert + expect(subject.extraTokenParams).toEqual({ 'resourceServer': 'abc' }); + }); + + it("should clear it if not object", () => { + // act + let subject = new OidcClientSettingsStore({ + client_id: 'client', + extraTokenParams: { + 'resourceServer': 'abc', + } + }); + subject.extraTokenParams = undefined; + + // assert + expect(subject.extraTokenParams).toEqual({}); + }); + }); +}); diff --git a/test/unit/ResponseValidator.spec.js b/test/unit/ResponseValidator.spec.js deleted file mode 100644 index 393e9dc75..000000000 --- a/test/unit/ResponseValidator.spec.js +++ /dev/null @@ -1,1028 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { ResponseValidator } from '../../src/ResponseValidator'; -import { Log } from '../../src/Log'; -import { JoseUtil } from '../../src/JoseUtil'; - -import { StubMetadataService } from './StubMetadataService'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; -let expect = chai.expect; - -class MockJoseUtility { - parseJwt(...args) { - this.parseJwtWasCalled = true; - if (this.parseJwtResult) { - Log.debug("MockJoseUtility.parseJwt", this.parseJwtResult) - return this.parseJwtResult; - } - return JoseUtil.parseJwt(...args); - } - - validateJwt(...args) { - this.validateJwtWasCalled = true; - if (this.validateJwtResult) { - Log.debug("MockJoseUtility.validateJwt", this.validateJwtResult) - return this.validateJwtResult; - } - return JoseUtil.validateJwt(...args); - } - - hashString(...args) { - this.hashStringWasCalled = true; - if (this.hashStringResult) { - Log.debug("MockJoseUtility.hashString", this.hashStringResult) - return this.hashStringResult; - } - return JoseUtil.hashString(...args); - } - - hexToBase64Url(...args) { - this.hexToBase64UrlCalled = true; - if (this.hexToBase64UrlResult) { - Log.debug("MockJoseUtility.hexToBase64Url", this.hexToBase64UrlResult) - return this.hexToBase64UrlResult; - } - return JoseUtil.hexToBase64Url(...args); - } -} - -class StubUserInfoService { - constructor() { - this.getClaimsWasCalled = false; - } - - getClaims() { - this.getClaimsWasCalled = true; - return this.getClaimsResult; - } -} - -class MockResponseValidator extends ResponseValidator { - constructor(settings, MetadataServiceCtor, UserInfoServiceCtor, joseUtil) { - super(settings, MetadataServiceCtor, UserInfoServiceCtor, joseUtil); - } - - _mock(name, ...args) { - Log.debug("mock called", name); - this[name + "WasCalled"] = true; - - if (this[name + "Result"]) { - Log.debug("mock returning result", this[name + "Result"]); - return this[name + "Result"]; - } - - Log.debug("mock calling super"); - return super[name](...args); - } - - _processSigninParams(...args) { - return this._mock("_processSigninParams", ...args); - } - _validateTokens(...args) { - return this._mock("_validateTokens", ...args); - } - _processClaims(...args) { - return this._mock("_processClaims", ...args); - } - _mergeClaims(...args) { - return this._mock("_mergeClaims", ...args); - } - - _getSigningKeyForJwt(...args) { - this._getSigningKeyForJwtSignedCalledCount = (this._getSigningKeyForJwtSignedCalledCount || 0) + 1; - return this._mock("_getSigningKeyForJwt", ...args); - } - _getSigningKeyForJwtWithSingleRetry(...args) { - this._getSigningKeyForJwtSignedCalledCount = 0; - return this._mock("_getSigningKeyForJwtWithSingleRetry", ...args); - } - - _validateIdTokenAndAccessToken(...args) { - return this._mock("_validateIdTokenAndAccessToken", ...args); - } - _validateIdToken(...args) { - return this._mock("_validateIdToken", ...args); - } - validateJwt(...args) { - return this._mock("validateJwt", ...args); - } - _validateAccessToken(...args) { - return this._mock("_validateAccessToken", ...args); - } - - _filterProtocolClaims(...args) { - return this._mock("_filterProtocolClaims", ...args); - } -} - -describe("ResponseValidator", function () { - let id_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; - - let access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzMvY29yZS9yZXNvdXJjZXMiLCJleHAiOjE0NTkxMzM1MDEsIm5iZiI6MTQ1OTEyOTkwMSwiY2xpZW50X2lkIjoianMudG9rZW5tYW5hZ2VyIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwicmVhZCIsIndyaXRlIl0sInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.ldCBx4xF_WIj6S9unppYAzXFKMs5ce7sKuse-nleFbzwRbZ-VNubLOlnpsFzquJIyTlGLekqLWnsfpAmaORQBtv5ZoaUHxC_s5APLWGC9Io19tF8NxWVmX2OK3cwHWQ5HtFkILQdYR9l3Bf5RIQK4ixbrKJN7OyzoLAen0FgEXDn-dXMAhFJDl123G7pBaayQb8ic44y808cfKlu3wwP2QkDEzgW-L0avvjN95zji5528c32L2LBMveRklcOXO6Gb0alcFw6PysfJotsNo9WahJWu404mSl3Afc-4jCWjoTL7PBL-xciPmq9iCNAgqVS7GN1s1WsnBW2R4kGLy-kcQ"; - - let at_hash = "JgDUCyoatJyEmGiiWbwOhA"; - - let settings; - let subject; - let stubMetadataService; - let stubUserInfoService; - let mockJoseUtility; - - let stubState; - let stubResponse; - - beforeEach(function () { - Log.logger = console; - Log.level = Log.NONE; - - stubState = { - id: "the_id", - nonce: "7221005209972382", - data: { some: 'data' }, - client_id: "client", - authority: "op" - }; - stubResponse = { - state: 'the_id', - isOpenIdConnect: false - }; - - settings = { - authority: "op", - client_id: 'client' - }; - stubMetadataService = new StubMetadataService(); - stubUserInfoService = new StubUserInfoService(); - mockJoseUtility = new MockJoseUtility(); - - subject = new MockResponseValidator(settings, () => stubMetadataService, () => stubUserInfoService, mockJoseUtility); - }); - - describe("constructor", function () { - - it("should require a settings param", function () { - try { - new ResponseValidator(undefined, () => stubMetadataService, () => stubUserInfoService); - } - catch (e) { - e.message.should.contain('settings'); - return; - } - assert.fail(); - }); - - }); - - describe("validateSignoutResponse", function () { - - it("should validate that the client state matches response state", function (done) { - - stubResponse.state = "not_the_id"; - subject.validateSignoutResponse(stubState, stubResponse).then(null, err => { - err.message.should.contain('match'); - done(); - }); - - }); - - it("should fail on error response", function (done) { - - stubResponse.error = "some_error"; - subject.validateSignoutResponse(stubState, stubResponse).then(null, err => { - err.error.should.equal("some_error"); - done(); - }); - - }); - - it("should return data for error responses", function (done) { - - stubResponse.error = "some_error"; - subject.validateSignoutResponse(stubState, stubResponse).then(null, err => { - err.state.should.deep.equal({ some: 'data' }); - done(); - }); - - }); - - it("should return data for successful responses", function (done) { - - subject.validateSignoutResponse(stubState, stubResponse).then(response => { - response.state.should.deep.equal({ some: 'data' }); - done(); - }); - - }); - - }); - - describe("validateSigninResponse", function () { - - it("should process signin params", function (done) { - - subject._processSigninParamsResult = Promise.resolve(stubResponse); - subject._validateTokensResult = Promise.resolve(stubResponse); - - subject.validateSigninResponse(stubState, stubResponse).then(response => { - subject._processSigninParamsWasCalled.should.be.true; - done(); - }); - - }); - - it("should validate tokens", function (done) { - - subject._processSigninParamsResult = Promise.resolve(stubResponse); - subject._validateTokensResult = Promise.resolve(stubResponse); - - subject.validateSigninResponse(stubState, stubResponse).then(response => { - subject._validateTokensWasCalled.should.be.true; - done(); - }); - - }); - - it("should not validate tokens if state fails", function (done) { - - subject._processSigninParamsResult = Promise.reject("error"); - subject._validateTokensResult = Promise.resolve(stubResponse); - - subject.validateSigninResponse(stubState, stubResponse).then(null, err => { - expect(subject._validateTokensWasCalled).to.be.undefined; - done(); - }); - - }); - - it("should process claims", function (done) { - - subject._processSigninParamsResult = Promise.resolve(stubResponse); - subject._validateTokensResult = Promise.resolve(stubResponse); - - subject.validateSigninResponse(stubState, stubResponse).then(response => { - subject._processClaimsWasCalled.should.be.true; - done(); - }); - - }); - - it("should not process claims if state fails", function (done) { - - subject._processSigninParamsResult = Promise.resolve(stubResponse); - subject._validateTokensResult = Promise.reject("error"); - - subject.validateSigninResponse(stubState, stubResponse).then(null, err => { - expect(subject._processClaimsWasCalled).to.be.undefined; - done(); - }); - - }); - - }); - - describe("_processSigninParams", function () { - - it("should fail if no authority on state", function (done) { - - delete stubState.authority; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain('authority'); - done(); - }); - }); - - it("should fail if no client_id on state", function (done) { - - delete stubState.client_id; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain('client_id'); - done(); - }); - }); - - it("should fail if the authority on the state is not the same as the settings", function (done) { - - stubState.authority = "something different"; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain('authority mismatch'); - done(); - }); - }); - - it("should fail if the client_id on the state is not the same as the settings", function (done) { - - stubState.client_id = "something different"; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain('client_id mismatch'); - done(); - }); - }); - - it("should assign the authority on the settings if not already assigned", function (done) { - - delete subject._settings.authority; - stubState.authority = "something different"; - - stubResponse.id_token = id_token; - - subject._processSigninParams(stubState, stubResponse).then(response => { - delete subject._settings.authority.should.equal("something different"); - done(); - }); - }); - - it("should assign the client_id on the settings if not already assigned", function (done) { - - delete subject._settings.client_id; - stubState.client_id = "something different"; - - stubResponse.id_token = id_token; - - subject._processSigninParams(stubState, stubResponse).then(response => { - delete subject._settings.client_id.should.equal("something different"); - done(); - }); - }); - - it("should validate that the client state matches response state", function (done) { - - stubResponse.state = "not_the_id"; - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain('match'); - done(); - }); - - }); - - it("should fail on error response", function (done) { - - stubResponse.error = "some_error"; - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.error.should.equal("some_error"); - done(); - }); - - }); - - it("should return data for error responses", function (done) { - - stubResponse.error = "some_error"; - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.state.should.deep.equal({ some: 'data' }); - done(); - }); - - }); - - it("should fail if request was OIDC but no id_token in response", function (done) { - - delete stubResponse.id_token; - stubResponse.isOpenIdConnect = true; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain("id_token"); - done(); - }); - - }); - - it("should fail if request was not OIDC but id_token in response", function (done) { - - delete stubState.nonce; - stubResponse.id_token = id_token; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain("id_token"); - done(); - }); - - }); - - it("should fail if request was code flow but no code in response", function (done) { - - stubResponse.id_token = id_token; - stubState.code_verifier = "secret"; - delete stubResponse.code; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain("code"); - done(); - }); - - }); - - it("should fail if request was not code flow no code in response", function (done) { - - stubResponse.id_token = id_token; - stubResponse.code = "code"; - - subject._processSigninParams(stubState, stubResponse).then(null, err => { - err.message.should.contain("code"); - done(); - }); - - }); - - it("should return data for successful responses", function (done) { - - stubResponse.id_token = id_token; - - subject._processSigninParams(stubState, stubResponse).then(response => { - response.state.should.deep.equal({ some: 'data' }); - done(); - }); - - }); - }); - - describe("_processClaims", function () { - - it("should filter protocol claims if OIDC", function (done) { - - stubResponse.isOpenIdConnect = true; - stubResponse.profile = { a: 'apple', b: 'banana' }; - - subject._processClaims({}, stubResponse).then(response => { - subject._filterProtocolClaimsWasCalled.should.be.true; - done(); - }); - - }); - - it("should not filter protocol claims if not OIDC", function (done) { - - stubResponse.isOpenIdConnect = false; - - subject._processClaims({}, stubResponse).then(response => { - assert.isUndefined(subject._filterProtocolClaimsWasCalled); - done(); - }); - - }); - - it("should load and merge user info claims when loadUserInfo configured", function (done) { - - settings.loadUserInfo = true; - - stubResponse.isOpenIdConnect = true; - stubResponse.profile = { a: 'apple', b: 'banana' }; - stubResponse.access_token = "access_token"; - stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); - - subject._processClaims({}, stubResponse).then(response => { - stubUserInfoService.getClaimsWasCalled.should.be.true; - subject._mergeClaimsWasCalled.should.be.true; - done(); - }); - - }); - - it("should not run if reqest was not openid", function (done) { - - settings.loadUserInfo = true; - - stubResponse.isOpenIdConnect = false; - stubResponse.profile = { a: 'apple', b: 'banana' }; - stubResponse.access_token = "access_token"; - stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); - - subject._processClaims({}, stubResponse).then(response => { - stubUserInfoService.getClaimsWasCalled.should.be.false; - done(); - }); - - }); - - it("should not load and merge user info claims when loadUserInfo not configured", function (done) { - - settings.loadUserInfo = false; - - stubResponse.isOpenIdConnect = true; - stubResponse.profile = { a: 'apple', b: 'banana' }; - stubResponse.access_token = "access_token"; - stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); - - subject._processClaims({}, stubResponse).then(response => { - stubUserInfoService.getClaimsWasCalled.should.be.false; - done(); - }); - - }); - - it("should not load user info claims if no access token", function (done) { - - settings.loadUserInfo = true; - - stubResponse.isOpenIdConnect = true; - stubResponse.profile = { a: 'apple', b: 'banana' }; - stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); - - subject._processClaims({}, stubResponse).then(response => { - stubUserInfoService.getClaimsWasCalled.should.be.false; - done(); - }); - - }); - - }); - - - describe("_mergeClaims", function () { - - it("should merge claims", function () { - - var c1 = { a: 'apple', b: 'banana' }; - var c2 = { c: 'carrot' }; - - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: 'apple', c: 'carrot', b: 'banana' }); - }); - - it("should not merge claims when claim types are objects", function () { - - var c1 = { custom: {'apple': 'foo', 'pear': 'bar'} }; - var c2 = { custom: {'apple': 'foo', 'orange': 'peel'}, b: 'banana' }; - - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ custom: [{'apple': 'foo', 'pear': 'bar'}, {'apple': 'foo', 'orange': 'peel'}], b: 'banana' }); - }); - - it("should merge claims when claim types are objects when mergeClaims settings is true", function () { - - settings.mergeClaims = true; - - var c1 = { custom: {'apple': 'foo', 'pear': 'bar'} }; - var c2 = { custom: {'apple': 'foo', 'orange': 'peel'}, b: 'banana' }; - - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ custom: {'apple': 'foo', 'pear': 'bar', 'orange': 'peel'}, b: 'banana' }); - }); - - it("should merge same claim types into array", function () { - - var c1 = { a: 'apple', b: 'banana' }; - var c2 = { a: 'carrot' }; - - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'carrot'], b: 'banana' }); - }); - - it("should merge arrays of same claim types into array", function () { - - var c1 = { a: 'apple', b: 'banana' }; - var c2 = { a: ['carrot', 'durian'] }; - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); - - var c1 = { a: ['apple', 'carrot'], b: 'banana' }; - var c2 = { a: ['durian'] }; - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); - - var c1 = { a: ['apple', 'carrot'], b: 'banana' }; - var c2 = { a: 'durian' }; - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); - }); - - it("should remove duplicates when producing arrays", function () { - - var c1 = { a: 'apple', b: 'banana' }; - var c2 = { a: ['apple', 'durian'] }; - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'durian'], b: 'banana' }); - }); - - it("should not add if already present in array", function () { - - var c1 = { a: ['apple', 'durian'], b: 'banana' }; - var c2 = { a: 'apple' }; - var result = subject._mergeClaims(c1, c2); - result.should.deep.equal({ a: ['apple', 'durian'], b: 'banana' }); - }); - }); - - describe("_filterProtocolClaims", function () { - - it("should filter protocol claims if enabled on settings", function () { - - settings._filterProtocolClaims = true; - let claims = { - foo: 1, bar: 'test', - aud: 'some_aud', iss: 'issuer', - sub: '123', email: 'foo@gmail.com', - role: ['admin', 'dev'], - nonce: 'nonce', at_hash: "athash", - iat: 5, nbf: 10, exp: 20 - }; - - var result = subject._filterProtocolClaims(claims); - result.should.deep.equal({ - foo: 1, bar: 'test', - sub: '123', email: 'foo@gmail.com', - role: ['admin', 'dev'] - }); - - }); - - it("should not filter protocol claims if not enabled on settings", function () { - - settings._filterProtocolClaims = false; - let claims = { - foo: 1, bar: 'test', - aud: 'some_aud', iss: 'issuer', - sub: '123', email: 'foo@gmail.com', - role: ['admin', 'dev'], - nonce: 'nonce', at_hash: "athash", - iat: 5, nbf: 10, exp: 20 - }; - - var result = subject._filterProtocolClaims(claims); - result.should.deep.equal({ - foo: 1, bar: 'test', - aud: 'some_aud', iss: 'issuer', - sub: '123', email: 'foo@gmail.com', - role: ['admin', 'dev'], - nonce: 'nonce', at_hash: "athash", - iat: 5, nbf: 10, exp: 20 - }); - - }); - }); - - describe("_validateTokens", function () { - - it("should validate id_token and access_token", function (done) { - - stubResponse.id_token = "id_token"; - stubResponse.access_token = "access_token"; - subject._validateIdTokenAndAccessTokenResult = Promise.resolve(stubResponse); - - subject._validateTokens(stubState, stubResponse).then(response => { - subject._validateIdTokenAndAccessTokenWasCalled.should.be.true; - expect(subject._validateIdTokenWasCalled).to.be.undefined; - done(); - }); - - }); - - it("should validate just id_token", function (done) { - - stubResponse.id_token = "id_token"; - subject._validateIdTokenResult = Promise.resolve(stubResponse); - - subject._validateTokens(stubState, stubResponse).then(response => { - subject._validateIdTokenWasCalled.should.be.true; - expect(subject._validateIdTokenAndAccessTokenWasCalled).to.be.undefined; - done(); - }); - - }); - - it("should not validate if only access_token", function (done) { - - stubResponse.access_token = "access_token"; - - subject._validateTokens(stubState, stubResponse).then(response => { - expect(subject._validateIdTokenWasCalled).to.be.undefined; - expect(subject._validateIdTokenAndAccessTokenWasCalled).to.be.undefined; - done(); - }); - - }); - }); - - describe("_validateIdTokenAndAccessToken", function () { - - it("should validate id_token and access_token", function (done) { - - stubResponse.id_token = id_token; - stubResponse.access_token = access_token; - stubResponse.profile = { - at_hash: at_hash - }; - subject._validateIdTokenResult = Promise.resolve(stubResponse); - - subject._validateIdTokenAndAccessToken(stubState, stubResponse).then(response => { - subject._validateIdTokenWasCalled.should.be.true; - subject._validateAccessTokenWasCalled.should.be.true; - done(); - }); - - }); - - it("should not access_token if id_token validation fails", function (done) { - - stubResponse.id_token = "id_token"; - stubResponse.access_token = "access_token"; - subject._validateIdTokenResult = Promise.reject(new Error("error")); - - subject._validateIdTokenAndAccessToken(stubState, stubResponse).then(null, err => { - subject._validateIdTokenWasCalled.should.be.true; - expect(subject._validateAccessTokenWasCalled).to.be.undefined; - done(); - }); - - }); - - }); - - describe("_getSigningKeyForJwt", function () { - - it("should fail if loading keys fails.", function (done) { - - const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; - stubMetadataService.getSigningKeysResult = Promise.reject(new Error("keys")); - - subject._getSigningKeyForJwt(jwt).then(null, err => { - err.message.should.contain('keys'); - done(); - }) - }) - - it("should fetch suitable signing key for the jwt.", function (done) { - - const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; - stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }, { kid: 'other_key' } ]) - - subject._getSigningKeyForJwt(jwt).then(key => { - key.should.deep.equal({ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }) - done(); - }) - }) - }) - - describe("_getSigningKeyForJwtWithSingleRetry", function () { - - it("should retry once if suitable signing key is not found.", function (done) { - - const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; - var callCount = 0 - stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'other_key' } ]) - - subject._getSigningKeyForJwtWithSingleRetry(jwt).then(key => { - subject._getSigningKeyForJwtSignedCalledCount.should.equal(2); - done(); - }) - }) - - it("should not retry if suitable signing key is found.", function (done) { - - const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; - var callCount = 0 - stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' } ]) - - subject._getSigningKeyForJwtWithSingleRetry(jwt).then(key => { - subject._getSigningKeyForJwtSignedCalledCount.should.equal(1); - done(); - }) - }) - }) - - describe("_validateIdToken", function () { - - it("should fail if no nonce on state", function (done) { - - delete stubState.nonce; - stubResponse.id_token = id_token; - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('nonce'); - done(); - }); - }); - - it("should fail if invalid id_token", function (done) { - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('id_token'); - done(); - }); - }); - - it("should fail if audience doesn't match id_token", function (done) { - - stubState.client_id = "invalid client_id"; - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.resolve("test"); - stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }]); - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - done(); - }); - }); - - it("should fail if nonce doesn't match id_token", function (done) { - - stubState.nonce = "invalid nonce"; - stubResponse.id_token = id_token; - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('nonce'); - done(); - }); - }); - - it("should fail if issuer fails", function (done) { - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.reject(new Error("issuer")); - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('issuer'); - done(); - }); - }); - - it("should fail if loading keys fails", function (done) { - - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.resolve("test"); - stubMetadataService.getSigningKeysResult = Promise.reject(new Error("keys")); - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('keys'); - done(); - }); - }); - - it("should fail if no matching key found in signing keys", function (done) { - - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.resolve("test"); - stubMetadataService.getSigningKeysResult = Promise.resolve([]); - - subject._validateIdToken(stubState, stubResponse).then(null, err => { - err.message.should.contain('kid'); - done(); - }); - }); - - it("should validate JWT", function (done) { - - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.resolve("test"); - stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }]); - - mockJoseUtility.validateJwtResult = Promise.resolve(); - - subject._validateIdToken(stubState, stubResponse).then(response => { - mockJoseUtility.validateJwtWasCalled.should.be.true; - done(); - }); - }); - - it("should set profile on result if successful", function (done) { - - stubResponse.id_token = id_token; - stubMetadataService.getIssuerResult = Promise.resolve("test"); - stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }]); - - mockJoseUtility.validateJwtResult = Promise.resolve(); - - subject._validateIdToken(stubState, stubResponse).then(response => { - response.profile.should.be.ok; - done(); - }); - }); - - }); - - describe("_validateAccessToken", function () { - - it("should require id_token", function (done) { - - stubResponse.id_token = null; - stubResponse.profile = { - at_hash: at_hash - }; - - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("id_token"); - done(); - }); - }); - - it("should require profile", function (done) { - - stubResponse.id_token = id_token; - stubResponse.profile = null; - - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("profile"); - done(); - }); - }); - - it("should require at_hash on profile", function (done) { - - stubResponse.id_token = id_token; - stubResponse.profile = { - }; - - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("at_hash"); - done(); - }); - }); - - it("should fail for invalid id_token", function (done) { - - stubResponse.id_token = "bad"; - stubResponse.profile = { - at_hash: at_hash - }; - - subject._validateAccessToken(stubResponse).then(null, err => { - Log.debug(err); - err.message.should.contain("id_token"); - done(); - }); - }); - - it("should require proper alg on id_token", function (done) { - - stubResponse.id_token = "bad"; - stubResponse.profile = { - at_hash: at_hash - }; - mockJoseUtility.parseJwtResult = { header: { alg: "bad" } }; - - subject._validateAccessToken(stubResponse).then(null, err => { - Log.debug(err); - err.message.should.contain("alg"); - done(); - }); - }); - - it("should fail for invalid algs of incorrect bit lengths", function (done) { - - stubResponse.id_token = id_token; - stubResponse.access_token = access_token; - stubResponse.profile = { - at_hash: at_hash - }; - - mockJoseUtility.parseJwtResult = { header: { alg: "HS123" } }; - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("alg"); - done(); - }); - }); - - it("should fail for algs of not correct string length", function (done) { - - stubResponse.id_token = id_token; - stubResponse.access_token = access_token; - stubResponse.profile = { - at_hash: at_hash - }; - - mockJoseUtility.parseJwtResult = { header: { alg: "abc" } }; - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("alg"); - done(); - }); - - }); - - it("should fail if at_hash does not match", function (done) { - - stubResponse.id_token = id_token; - stubResponse.access_token = access_token; - stubResponse.profile = { - at_hash: at_hash - }; - mockJoseUtility.parseJwtResult = { header: { alg: "RS256" } }; - mockJoseUtility.hashStringResult = "hash"; - mockJoseUtility.hexToBase64UrlResult = "wrong"; - - subject._validateAccessToken(stubResponse).then(null, err => { - err.message.should.contain("at_hash"); - done(); - }); - }); - - it("should validate at_hash", function (done) { - - stubResponse.id_token = id_token; - stubResponse.access_token = access_token; - stubResponse.profile = { - at_hash: at_hash - }; - - subject._validateAccessToken(stubResponse).then(response => { - response.should.be.deep.equal(stubResponse); - done(); - }); - }); - - }); -}); diff --git a/test/unit/ResponseValidator.test.ts b/test/unit/ResponseValidator.test.ts new file mode 100644 index 000000000..696d4e9f8 --- /dev/null +++ b/test/unit/ResponseValidator.test.ts @@ -0,0 +1,1215 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { JoseUtil } from '../../src/JoseUtil'; +import { ResponseValidator } from '../../src/ResponseValidator'; + +import { StubMetadataService } from './StubMetadataService'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +class MockJoseUtility { + parseJwtWasCalled: any; + parseJwtResult: any; + validateJwtWasCalled: any; + validateJwtResult: any; + hashStringWasCalled: any; + hashStringResult: any; + hexToBase64UrlCalled: any; + hexToBase64UrlResult: any; + + parseJwt(jwt: any) { + this.parseJwtWasCalled = true; + if (this.parseJwtResult) { + Log.debug("MockJoseUtility.parseJwt", this.parseJwtResult) + return this.parseJwtResult; + } + return JoseUtil.parseJwt(jwt); + } + + validateJwt(jwt: any, key: string, issuer: string, audience: string, clockSkew: number, now?: number, timeInsensitive = false) { + this.validateJwtWasCalled = true; + if (this.validateJwtResult) { + Log.debug("MockJoseUtility.validateJwt", this.validateJwtResult) + return this.validateJwtResult; + } + return JoseUtil.validateJwt(jwt, key, issuer, audience, clockSkew, now, timeInsensitive); + } + + hashString(value: any, alg: string) { + this.hashStringWasCalled = true; + if (this.hashStringResult) { + Log.debug("MockJoseUtility.hashString", this.hashStringResult) + return this.hashStringResult; + } + return JoseUtil.hashString(value, alg); + } + + hexToBase64Url(value: any) { + this.hexToBase64UrlCalled = true; + if (this.hexToBase64UrlResult) { + Log.debug("MockJoseUtility.hexToBase64Url", this.hexToBase64UrlResult) + return this.hexToBase64UrlResult; + } + return JoseUtil.hexToBase64Url(value); + } +} + +class StubUserInfoService { + getClaimsWasCalled: boolean; + getClaimsResult: any; + + constructor() { + this.getClaimsWasCalled = false; + } + + getClaims() { + this.getClaimsWasCalled = true; + return this.getClaimsResult; + } +} + +// TODO: port-ts - replace with jest.mock +class MockResponseValidator extends ResponseValidator { + private _getSigningKeyForJwtSignedCalledCount: any; + + constructor(settings: any, MetadataServiceCtor: any, UserInfoServiceCtor: any, joseUtil: any) { + super(settings, MetadataServiceCtor, UserInfoServiceCtor, joseUtil); + } + + _mock(name: string, ...args: any[]) { + Log.debug("mock called", name); + + // @ts-ignore + this[name + "WasCalled"] = true; + + // @ts-ignore + const result = this[name + "Result"] + if (result) { + Log.debug("mock returning result", result); + return result; + } + + Log.debug("mock calling super"); + // @ts-ignore + return super[name](...args); + } + + _processSigninParams(...args: any[]) { + return this._mock("_processSigninParams", ...args); + } + _validateTokens(...args: any[]) { + return this._mock("_validateTokens", ...args); + } + _processClaims(...args: any[]) { + return this._mock("_processClaims", ...args); + } + _mergeClaims(...args: any[]) { + return this._mock("_mergeClaims", ...args); + } + + _getSigningKeyForJwt(...args: any[]) { + this._getSigningKeyForJwtSignedCalledCount = (this._getSigningKeyForJwtSignedCalledCount || 0) + 1; + return this._mock("_getSigningKeyForJwt", ...args); + } + _getSigningKeyForJwtWithSingleRetry(...args: any[]) { + this._getSigningKeyForJwtSignedCalledCount = 0; + return this._mock("_getSigningKeyForJwtWithSingleRetry", ...args); + } + + _validateIdTokenAndAccessToken(...args: any[]) { + return this._mock("_validateIdTokenAndAccessToken", ...args); + } + _validateIdToken(...args: any[]) { + return this._mock("_validateIdToken", ...args); + } + validateJwt(...args: any[]) { + return this._mock("validateJwt", ...args); + } + _validateAccessToken(...args: any[]) { + return this._mock("_validateAccessToken", ...args); + } + + _filterProtocolClaims(...args: any[]) { + return this._mock("_filterProtocolClaims", ...args); + } +} + +describe("ResponseValidator", () => { + let id_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoianMudG9rZW5tYW5hZ2VyIiwiZXhwIjoxNDU5MTMwMjAxLCJuYmYiOjE0NTkxMjk5MDEsIm5vbmNlIjoiNzIyMTAwNTIwOTk3MjM4MiIsImlhdCI6MTQ1OTEyOTkwMSwiYXRfaGFzaCI6IkpnRFVDeW9hdEp5RW1HaWlXYndPaEEiLCJzaWQiOiIwYzVmMDYxZTYzOThiMWVjNmEwYmNlMmM5NDFlZTRjNSIsInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.f6S1Fdd0UQScZAFBzXwRiVsUIPQnWZLSe07kdtjANRZDZXf5A7yDtxOftgCx5W0ONQcDFVpLGPgTdhp7agZkPpCFutzmwr0Rr9G7E7mUN4xcIgAABhmRDfzDayFBEu6VM8wEWTChezSWtx2xG_2zmVJxxmNV0jvkaz0bu7iin-C_UZg6T-aI9FZDoKRGXZP9gF65FQ5pQ4bCYQxhKcvjjUfs0xSHGboL7waN6RfDpO4vvVR1Kz-PQhIRyFAJYRuoH4PdMczHYtFCb-k94r-7TxEU0vp61ww4WntbPvVWwUbCUgsEtmDzAZT-NEJVhWztNk1ip9wDPXzZ2hEhDAPJ7A"; + let access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSIsImtpZCI6ImEzck1VZ01Gdjl0UGNsTGE2eUYzekFrZnF1RSJ9.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo0NDMzMy9jb3JlIiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzMvY29yZS9yZXNvdXJjZXMiLCJleHAiOjE0NTkxMzM1MDEsIm5iZiI6MTQ1OTEyOTkwMSwiY2xpZW50X2lkIjoianMudG9rZW5tYW5hZ2VyIiwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwicmVhZCIsIndyaXRlIl0sInN1YiI6Ijg4NDIxMTEzIiwiYXV0aF90aW1lIjoxNDU5MTI5ODk4LCJpZHAiOiJpZHNydiIsImFtciI6WyJwYXNzd29yZCJdfQ.ldCBx4xF_WIj6S9unppYAzXFKMs5ce7sKuse-nleFbzwRbZ-VNubLOlnpsFzquJIyTlGLekqLWnsfpAmaORQBtv5ZoaUHxC_s5APLWGC9Io19tF8NxWVmX2OK3cwHWQ5HtFkILQdYR9l3Bf5RIQK4ixbrKJN7OyzoLAen0FgEXDn-dXMAhFJDl123G7pBaayQb8ic44y808cfKlu3wwP2QkDEzgW-L0avvjN95zji5528c32L2LBMveRklcOXO6Gb0alcFw6PysfJotsNo9WahJWu404mSl3Afc-4jCWjoTL7PBL-xciPmq9iCNAgqVS7GN1s1WsnBW2R4kGLy-kcQ"; + let at_hash = "JgDUCyoatJyEmGiiWbwOhA"; + + let settings: any; + let subject: MockResponseValidator; + let stubMetadataService: any; + let stubUserInfoService: any; + let mockJoseUtility: any; + + let stubState: any; + let stubResponse: any; + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + + stubState = { + id: "the_id", + nonce: "7221005209972382", + data: { some: 'data' }, + client_id: "client", + authority: "op" + }; + stubResponse = { + state: 'the_id', + isOpenIdConnect: false + }; + + settings = { + authority: "op", + client_id: 'client' + }; + stubMetadataService = new StubMetadataService(); + stubUserInfoService = new StubUserInfoService(); + mockJoseUtility = new MockJoseUtility(); + + subject = new MockResponseValidator(settings, () => stubMetadataService, () => stubUserInfoService, mockJoseUtility); + }); + + describe("validateSignoutResponse", () => { + + it("should validate that the client state matches response state", async () => { + // arrange + stubResponse.state = "not_the_id"; + + // act + try { + await subject.validateSignoutResponse(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('match'); + } + }); + + it("should fail on error response", async () => { + // arrange + stubResponse.error = "some_error"; + + // act + try { + await subject.validateSignoutResponse(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.error).toEqual('some_error'); + } + }); + + it("should return data for error responses", async () => { + // arrange + stubResponse.error = "some_error"; + + // act + try { + await subject.validateSignoutResponse(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.state).toEqual({ some: 'data' }); + } + }); + + it("should return data for successful responses", async () => { + // act + const response = await subject.validateSignoutResponse(stubState, stubResponse); + + // assert + expect(response.state).toEqual({ some: 'data' }); + }); + }); + + describe("validateSigninResponse", () => { + + it("should process signin params", async () => { + // arrange + // @ts-ignore + subject._processSigninParamsResult = Promise.resolve(stubResponse); + // @ts-ignore + subject._validateTokensResult = Promise.resolve(stubResponse); + + // act + await subject.validateSigninResponse(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._processSigninParamsWasCalled).toEqual(true); + }); + + it("should validate tokens", async () => { + // arrange + // @ts-ignore + subject._processSigninParamsResult = Promise.resolve(stubResponse); + // @ts-ignore + subject._validateTokensResult = Promise.resolve(stubResponse); + + // act + await subject.validateSigninResponse(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._validateTokensWasCalled).toEqual(true); + }); + + it("should not validate tokens if state fails", async () => { + // arrange + // @ts-ignore + subject._processSigninParamsResult = Promise.reject("error"); + // @ts-ignore + subject._validateTokensResult = Promise.resolve(stubResponse); + + // act + try { + await subject.validateSigninResponse(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + // @ts-ignore + expect(subject._validateTokensWasCalled).toBeUndefined(); + } + }); + + it("should process claims", async () => { + // arrange + // @ts-ignore + subject._processSigninParamsResult = Promise.resolve(stubResponse); + // @ts-ignore + subject._validateTokensResult = Promise.resolve(stubResponse); + + // act + await subject.validateSigninResponse(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._processClaimsWasCalled).toEqual(true); + }); + + it("should not process claims if state fails", async () => { + // arrange + // @ts-ignore + subject._processSigninParamsResult = Promise.resolve(stubResponse); + // @ts-ignore + subject._validateTokensResult = Promise.reject("error"); + + // act + try { + await subject.validateSigninResponse(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + // @ts-ignore + expect(subject._processClaimsWasCalled).toBeUndefined(); + } + }); + }); + + describe("_processSigninParams", () => { + + it("should fail if no authority on state", async () => { + // arrange + delete stubState.authority; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('authority'); + } + }); + + it("should fail if no client_id on state", async () => { + // arrange + delete stubState.client_id; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('client_id'); + } + }); + + it("should fail if the authority on the state is not the same as the settings", async () => { + // arrange + stubState.authority = "something different"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('authority mismatch'); + } + }); + + it("should fail if the client_id on the state is not the same as the settings", async () => { + // arrange + stubState.client_id = "something different"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('client_id mismatch'); + } + }); + + it("should assign the authority on the settings if not already assigned", async () => { + // arrange + delete settings.authority; + stubState.authority = "something different"; + stubResponse.id_token = id_token; + subject = new MockResponseValidator(settings, () => stubMetadataService, () => stubUserInfoService, mockJoseUtility); + + // act + await subject._processSigninParams(stubState, stubResponse); + + // assert + expect(settings.authority).toEqual("something different"); + }); + + it("should assign the client_id on the settings if not already assigned", async () => { + // arrange + delete settings.client_id; + stubState.client_id = "something different"; + stubResponse.id_token = id_token; + subject = new MockResponseValidator(settings, () => stubMetadataService, () => stubUserInfoService, mockJoseUtility); + + // act + await subject._processSigninParams(stubState, stubResponse); + + // assert + expect(settings.client_id).toEqual("something different"); + }); + + it("should validate that the client state matches response state", async () => { + // arrange + stubResponse.state = "not_the_id"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('match'); + } + }); + + it("should fail on error response", async () => { + // arrange + stubResponse.error = "some_error"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.error).toEqual("some_error"); + } + }); + + it("should return data for error responses", async () => { + // arrange + stubResponse.error = "some_error"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.state).toEqual({ some: 'data' }); + } + }); + + it("should fail if request was OIDC but no id_token in response", async () => { + // arrange + delete stubResponse.id_token; + stubResponse.isOpenIdConnect = true; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("id_token"); + } + }); + + it("should fail if request was not OIDC but id_token in response", async () => { + // arrange + delete stubState.nonce; + stubResponse.id_token = id_token; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("id_token"); + } + }); + + it("should fail if request was code flow but no code in response", async () => { + // arrange + stubResponse.id_token = id_token; + stubState.code_verifier = "secret"; + delete stubResponse.code; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("code"); + } + }); + + it("should fail if request was not code flow no code in response", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.code = "code"; + + // act + try { + await subject._processSigninParams(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("code"); + } + }); + + it("should return data for successful responses", async () => { + // arrange + stubResponse.id_token = id_token; + + // act + const response = await subject._processSigninParams(stubState, stubResponse); + + // assert + expect(response.state).toEqual({ some: 'data' }); + }); + }); + + describe("_processClaims", () => { + + it("should filter protocol claims if OIDC", async () => { + // arrange + stubResponse.isOpenIdConnect = true; + stubResponse.profile = { a: 'apple', b: 'banana' }; + + // act + await subject._processClaims({}, stubResponse); + + // assert + // @ts-ignore + expect(subject._filterProtocolClaimsWasCalled).toEqual(true); + }); + + it("should not filter protocol claims if not OIDC", async () => { + // arrange + stubResponse.isOpenIdConnect = false; + + // act + await subject._processClaims({}, stubResponse); + + // assert + // @ts-ignore + expect(subject._filterProtocolClaimsWasCalled).toBeUndefined(); + }); + + it("should load and merge user info claims when loadUserInfo configured", async () => { + // arrange + settings.loadUserInfo = true; + + stubResponse.isOpenIdConnect = true; + stubResponse.profile = { a: 'apple', b: 'banana' }; + stubResponse.access_token = "access_token"; + stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); + + // act + await subject._processClaims({}, stubResponse); + + // assert + expect(stubUserInfoService.getClaimsWasCalled).toEqual(true); + // @ts-ignore + expect(subject._mergeClaimsWasCalled).toEqual(true); + }); + + it("should not run if reqest was not openid", async () => { + // arrange + settings.loadUserInfo = true; + + stubResponse.isOpenIdConnect = false; + stubResponse.profile = { a: 'apple', b: 'banana' }; + stubResponse.access_token = "access_token"; + stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); + + // act + await subject._processClaims({}, stubResponse); + + // assert + expect(stubUserInfoService.getClaimsWasCalled).toEqual(false); + }); + + it("should not load and merge user info claims when loadUserInfo not configured", async () => { + // arrange + settings.loadUserInfo = false; + + stubResponse.isOpenIdConnect = true; + stubResponse.profile = { a: 'apple', b: 'banana' }; + stubResponse.access_token = "access_token"; + stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); + + // act + await subject._processClaims({}, stubResponse); + + // assert + expect(stubUserInfoService.getClaimsWasCalled).toEqual(false); + }); + + it("should not load user info claims if no access token", async () => { + // arrange + settings.loadUserInfo = true; + + stubResponse.isOpenIdConnect = true; + stubResponse.profile = { a: 'apple', b: 'banana' }; + stubUserInfoService.getClaimsResult = Promise.resolve({ c: 'carrot' }); + + // act + await subject._processClaims({}, stubResponse); + + // assert + expect(stubUserInfoService.getClaimsWasCalled).toEqual(false); + }); + }); + + describe("_mergeClaims", () => { + + it("should merge claims", () => { + // arrange + var c1 = { a: 'apple', b: 'banana' }; + var c2 = { c: 'carrot' }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ a: 'apple', c: 'carrot', b: 'banana' }); + }); + + it("should not merge claims when claim types are objects", () => { + // arrange + var c1 = { custom: {'apple': 'foo', 'pear': 'bar'} }; + var c2 = { custom: {'apple': 'foo', 'orange': 'peel'}, b: 'banana' }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ custom: [{'apple': 'foo', 'pear': 'bar'}, {'apple': 'foo', 'orange': 'peel'}], b: 'banana' }); + }); + + it("should merge claims when claim types are objects when mergeClaims settings is true", () => { + // arrange + settings.mergeClaims = true; + + var c1 = { custom: {'apple': 'foo', 'pear': 'bar'} }; + var c2 = { custom: {'apple': 'foo', 'orange': 'peel'}, b: 'banana' }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ custom: {'apple': 'foo', 'pear': 'bar', 'orange': 'peel'}, b: 'banana' }); + }); + + it("should merge same claim types into array", () => { + // arrange + var c1 = { a: 'apple', b: 'banana' }; + var c2 = { a: 'carrot' }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ a: ['apple', 'carrot'], b: 'banana' }); + }); + + it("should merge arrays of same claim types into array", () => { + // arrange + const c1 = { a: 'apple', b: 'banana' }; + const c2 = { a: ['carrot', 'durian'] }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); + + // arrange + const d1 = { a: ['apple', 'carrot'], b: 'banana' }; + const d2 = { a: ['durian'] }; + + // act + var result = subject._mergeClaims(d1, d2); + + // assert + expect(result).toEqual({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); + + // arrange + const e1 = { a: ['apple', 'carrot'], b: 'banana' }; + const e2 = { a: 'durian' }; + + // act + var result = subject._mergeClaims(e1, e2); + + // assert + expect(result).toEqual({ a: ['apple', 'carrot', 'durian'], b: 'banana' }); + }); + + it("should remove duplicates when producing arrays", () => { + // arrange + var c1 = { a: 'apple', b: 'banana' }; + var c2 = { a: ['apple', 'durian'] }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ a: ['apple', 'durian'], b: 'banana' }); + }); + + it("should not add if already present in array", () => { + // arrange + var c1 = { a: ['apple', 'durian'], b: 'banana' }; + var c2 = { a: 'apple' }; + + // act + var result = subject._mergeClaims(c1, c2); + + // assert + expect(result).toEqual({ a: ['apple', 'durian'], b: 'banana' }); + }); + }); + + describe("_filterProtocolClaims", () => { + + it("should filter protocol claims if enabled on settings", () => { + // arrange + settings.filterProtocolClaims = true; + let claims = { + foo: 1, bar: 'test', + aud: 'some_aud', iss: 'issuer', + sub: '123', email: 'foo@gmail.com', + role: ['admin', 'dev'], + nonce: 'nonce', at_hash: "athash", + iat: 5, nbf: 10, exp: 20 + }; + + // act + var result = subject._filterProtocolClaims(claims); + + // assert + expect(result).toEqual({ + foo: 1, bar: 'test', + sub: '123', email: 'foo@gmail.com', + role: ['admin', 'dev'] + }); + }); + + it("should not filter protocol claims if not enabled on settings", () => { + // arrange + settings.filterProtocolClaims = false; + let claims = { + foo: 1, bar: 'test', + aud: 'some_aud', iss: 'issuer', + sub: '123', email: 'foo@gmail.com', + role: ['admin', 'dev'], + nonce: 'nonce', at_hash: "athash", + iat: 5, nbf: 10, exp: 20 + }; + + // act + var result = subject._filterProtocolClaims(claims); + + // assert + expect(result).toEqual({ + foo: 1, bar: 'test', + aud: 'some_aud', iss: 'issuer', + sub: '123', email: 'foo@gmail.com', + role: ['admin', 'dev'], + nonce: 'nonce', at_hash: "athash", + iat: 5, nbf: 10, exp: 20 + }); + }); + }); + + describe("_validateTokens", () => { + + it("should validate id_token and access_token", async () => { + // arrange + stubResponse.id_token = "id_token"; + stubResponse.access_token = "access_token"; + // @ts-ignore + subject._validateIdTokenAndAccessTokenResult = Promise.resolve(stubResponse); + + // act + subject._validateTokens(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._validateIdTokenAndAccessTokenWasCalled).toEqual(true); + // @ts-ignore + expect(subject._validateIdTokenWasCalled).toBeUndefined(); + }); + + it("should validate just id_token", async () => { + // arrange + stubResponse.id_token = "id_token"; + // @ts-ignore + subject._validateIdTokenResult = Promise.resolve(stubResponse); + + // act + subject._validateTokens(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._validateIdTokenWasCalled).toEqual(true); + // @ts-ignore + expect(subject._validateIdTokenAndAccessTokenWasCalled).toBeUndefined(); + }); + + it("should not validate if only access_token", async () => { + // arrange + stubResponse.access_token = "access_token"; + + // act + subject._validateTokens(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._validateIdTokenWasCalled).toBeUndefined(); + // @ts-ignore + expect(subject._validateIdTokenAndAccessTokenWasCalled).toBeUndefined(); + }); + }); + + describe("_validateIdTokenAndAccessToken", () => { + + it("should validate id_token and access_token", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.access_token = access_token; + stubResponse.profile = { + at_hash: at_hash + }; + // @ts-ignore + subject._validateIdTokenResult = Promise.resolve(stubResponse); + // @ts-ignore + subject._validateAccessTokenResult = Promise.resolve(stubResponse); + + // act + await subject._validateIdTokenAndAccessToken(stubState, stubResponse); + + // assert + // @ts-ignore + expect(subject._validateIdTokenWasCalled).toEqual(true); + // @ts-ignore + expect(subject._validateAccessTokenWasCalled).toEqual(true); + }); + + it("should not access_token if id_token validation fails", async () => { + // arrange + stubResponse.id_token = "id_token"; + stubResponse.access_token = "access_token"; + // @ts-ignore + subject._validateIdTokenResult = Promise.reject(new Error("error")); + + // act + try { + await subject._validateIdTokenAndAccessToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + // @ts-ignore + expect(subject._validateIdTokenWasCalled).toEqual(true); + // @ts-ignore + expect(subject._validateAccessTokenWasCalled).toBeUndefined(); + } + }); + }); + + describe("_getSigningKeyForJwt", () => { + + it("should fail if loading keys fails.", async () => { + // arrange + const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; + stubMetadataService.getSigningKeysResult = Promise.reject(new Error("keys")); + + // act + try { + await subject._getSigningKeyForJwt(jwt); + fail("should not come here") + } catch (err) { + expect(err.message).toContain('keys'); + } + }) + + it("should fetch suitable signing key for the jwt.", async () => { + // arrange + const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; + stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }, { kid: 'other_key' } ]) + + // act + const result = await subject._getSigningKeyForJwt(jwt); + + // assert + expect(result).toEqual({ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }); + }) + }) + + describe("_getSigningKeyForJwtWithSingleRetry", () => { + + it("should retry once if suitable signing key is not found.", async () => { + // arrange + const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; + stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'other_key' } ]) + + // act + await subject._getSigningKeyForJwtWithSingleRetry(jwt); + + // assert + // @ts-ignore + expect(subject._getSigningKeyForJwtSignedCalledCount).toEqual(2); + }) + + it("should not retry if suitable signing key is found.", async () => { + // arrange + const jwt = { header: { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }}; + stubMetadataService.getSigningKeysResult = Promise.resolve([ { kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' } ]) + + // act + await subject._getSigningKeyForJwtWithSingleRetry(jwt); + + // assert + // @ts-ignore + expect(subject._getSigningKeyForJwtSignedCalledCount).toEqual(1); + }) + }) + + describe("_validateIdToken", () => { + + it("should fail if no nonce on state", async () => { + // arrange + delete stubState.nonce; + stubResponse.id_token = id_token; + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('nonce'); + } + }); + + it("should fail if invalid id_token", async () => { + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('id_token'); + } + }); + + it("should fail if audience doesn't match id_token", async () => { + // arrange + stubState.client_id = "invalid client_id"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.resolve("test"); + stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE' }]); + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + } + }); + + it("should fail if nonce doesn't match id_token", async () => { + // arrange + stubState.nonce = "invalid nonce"; + stubResponse.id_token = id_token; + mockJoseUtility.parseJwtResult = { header: { alg: "HS123" }, payload: "payload" }; + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('nonce'); + } + }); + + it("should fail if issuer fails", async () => { + // arrange + stubState.nonce = "nonce"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.reject(new Error("issuer")); + mockJoseUtility.parseJwtResult = { header: { alg: "HS123" }, payload: { nonce: stubState.nonce } }; + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('issuer'); + } + }); + + it("should fail if loading keys fails", async () => { + // arrange + stubState.nonce = "nonce"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.resolve("test"); + stubMetadataService.getSigningKeysResult = Promise.reject(new Error("keys")); + mockJoseUtility.parseJwtResult = { header: { alg: "HS123" }, payload: { nonce: stubState.nonce } }; + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('keys'); + } + }); + + it("should fail if no matching key found in signing keys", async () => { + // arrange + stubState.nonce = "nonce"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.resolve("test"); + stubMetadataService.getSigningKeysResult = Promise.resolve([]); + mockJoseUtility.parseJwtResult = { header: { alg: "HS123" }, payload: { nonce: stubState.nonce } }; + + // act + try { + await subject._validateIdToken(stubState, stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain('kid'); + } + }); + + it("should validate JWT", async () => { + // arrange + stubState.nonce = "nonce"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.resolve("test"); + stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE', kty: "EC" }]); + mockJoseUtility.validateJwtResult = Promise.resolve(); + mockJoseUtility.parseJwtResult = { header: { alg: "ES123" }, payload: { nonce: stubState.nonce, sub: "sub" } }; + + // act + await subject._validateIdToken(stubState, stubResponse); + + // assert + expect(mockJoseUtility.validateJwtWasCalled).toEqual(true); + }); + + it("should set profile on result if successful", async () => { + // arrange + stubState.nonce = "nonce"; + stubResponse.id_token = id_token; + stubMetadataService.getIssuerResult = Promise.resolve("test"); + stubMetadataService.getSigningKeysResult = Promise.resolve([{ kid: 'a3rMUgMFv9tPclLa6yF3zAkfquE', kty: "EC" }]); + mockJoseUtility.validateJwtResult = Promise.resolve(); + mockJoseUtility.parseJwtResult = { header: { alg: "ES123" }, payload: { nonce: stubState.nonce, sub: "sub" } }; + + // act + const response = await subject._validateIdToken(stubState, stubResponse); + + // assert + expect(response.profile).toBeDefined(); + }); + }); + + describe("_validateAccessToken", () => { + + it("should require id_token", async () => { + // arrange + stubResponse.id_token = null; + stubResponse.profile = { + at_hash: at_hash + }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("id_token"); + } + }); + + it("should require profile", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.profile = null; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("profile"); + } + }); + + it("should require at_hash on profile", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.profile = { + }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("at_hash"); + } + }); + + it("should fail for invalid id_token", async () => { + // arrange + stubResponse.id_token = "bad"; + stubResponse.profile = { + at_hash: at_hash + }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("id_token"); + } + }); + + it("should require proper alg on id_token", async () => { + // arrange + stubResponse.id_token = "bad"; + stubResponse.profile = { + at_hash: at_hash + }; + mockJoseUtility.parseJwtResult = { header: { alg: "bad" } }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("alg"); + } + }); + + it("should fail for invalid algs of incorrect bit lengths", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.access_token = access_token; + stubResponse.profile = { + at_hash: at_hash + }; + + mockJoseUtility.parseJwtResult = { header: { alg: "HS123" } }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("alg"); + } + }); + + it("should fail for algs of not correct string length", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.access_token = access_token; + stubResponse.profile = { + at_hash: at_hash + }; + + mockJoseUtility.parseJwtResult = { header: { alg: "abc" } }; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("alg"); + } + }); + + it("should fail if at_hash does not match", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.access_token = access_token; + stubResponse.profile = { + at_hash: at_hash + }; + mockJoseUtility.parseJwtResult = { header: { alg: "RS256" } }; + mockJoseUtility.hashStringResult = "hash"; + mockJoseUtility.hexToBase64UrlResult = "wrong"; + + // act + try { + await subject._validateAccessToken(stubResponse); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("at_hash"); + } + }); + + it("should validate at_hash", async () => { + // arrange + stubResponse.id_token = id_token; + stubResponse.access_token = access_token; + stubResponse.profile = { + at_hash: at_hash + }; + + // TODO: port-ts - once jsrsasign is avaiable from jest this lines can be removed + mockJoseUtility.parseJwtResult = { header: { alg: "ES512" } }; + mockJoseUtility.hashStringResult = "bla"; + mockJoseUtility.hexToBase64UrlResult = stubResponse.profile.at_hash; + + // act + const response = await subject._validateAccessToken(stubResponse); + + // assert + expect(response).toEqual(stubResponse); + }); + }); +}); diff --git a/test/unit/SigninRequest.spec.js b/test/unit/SigninRequest.spec.js deleted file mode 100644 index 103d42b7c..000000000 --- a/test/unit/SigninRequest.spec.js +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { SigninRequest } from '../../src/SigninRequest'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("SigninRequest", function() { - - let subject; - let settings; - - beforeEach(function() { - settings = {url: "http://sts/signin", - client_id: "client", - redirect_uri: "http://app", - response_type: "id_token", - scope: "openid", - authority : "op", - data: {data: "test"} - }; - subject = new SigninRequest(settings); - }); - - describe("constructor", function() { - - it("should require a url param", function() { - try { - delete settings.url; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('url'); - return; - } - assert.fail(); - }); - - it("should require a client_id param", function() { - try { - delete settings.client_id; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('client_id'); - return; - } - assert.fail(); - }); - - it("should require a redirect_uri param", function() { - try { - delete settings.redirect_uri; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('redirect_uri'); - return; - } - assert.fail(); - }); - - it("should require a response_type param", function() { - try { - delete settings.response_type; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('response_type'); - return; - } - assert.fail(); - }); - - it("should require a scope param", function() { - try { - delete settings.scope; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('scope'); - return; - } - assert.fail(); - }); - - it("should require a authority param", function() { - try { - delete settings.authority; - new SigninRequest(settings); - } - catch (e) { - e.message.should.contain('authority'); - return; - } - assert.fail(); - }); - - }); - - describe("url", function() { - - it("should include url", function() { - subject.url.indexOf("http://sts/signin").should.equal(0); - }); - - it("should include client_id", function() { - subject.url.should.contain("client_id=client"); - }); - - it("should include redirect_uri", function() { - subject.url.should.contain("redirect_uri=" + encodeURIComponent("http://app")); - }); - - it("should include response_type", function() { - subject.url.should.contain("response_type=id_token"); - }); - - it("should include scope", function() { - subject.url.should.contain("scope=openid"); - }); - - it("should include state", function() { - subject.url.should.contain("state=" + subject.state.id); - }); - - it("should include prompt", function() { - settings.prompt = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("prompt=foo"); - }); - - it("should include display", function() { - settings.display = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("display=foo"); - }); - - it("should include max_age", function() { - settings.max_age = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("max_age=foo"); - }); - - it("should include ui_locales", function() { - settings.ui_locales = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("ui_locales=foo"); - }); - - it("should include id_token_hint", function() { - settings.id_token_hint = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("id_token_hint=foo"); - }); - - it("should include login_hint", function() { - settings.login_hint = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("login_hint=foo"); - }); - - it("should include acr_values", function() { - settings.acr_values = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("acr_values=foo"); - }); - - it("should include resource", function() { - settings.resource = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("resource=foo"); - }); - - it("should include response_mode", function() { - settings.response_mode = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("response_mode=foo"); - }); - - it("should include request", function() { - settings.request = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("request=foo"); - }); - - it("should include request_uri", function() { - settings.request_uri = "foo"; - subject = new SigninRequest(settings); - subject.url.should.contain("request_uri=foo"); - }); - - it("should include extra query params", function() { - settings.extraQueryParams = { - 'hd': 'domain.com', - 'foo': 'bar' - }; - subject = new SigninRequest(settings); - subject.url.should.contain('hd=domain.com&foo=bar'); - }); - - it("should store extra token params in state", function() { - settings.extraTokenParams = { - 'resourceServer': 'abc', - }; - subject = new SigninRequest(settings); - assert.deepEqual(subject.state.extraTokenParams, { - 'resourceServer': 'abc' - }); - }); - - it("should include code flow params", function() { - settings.response_type = "code"; - subject = new SigninRequest(settings); - subject.url.should.contain("code_challenge="); - subject.url.should.contain("code_challenge_method=S256"); - }); - - it("should include hybrid flow params", function() { - settings.response_type = "code id_token"; - subject = new SigninRequest(settings); - subject.url.should.contain("nonce="); - subject.url.should.contain("code_challenge="); - subject.url.should.contain("code_challenge_method=S256"); - }); - }); - - describe("isOidc", function() { - it("should indicate if response_type is oidc", function() { - SigninRequest.isOidc("id_token").should.be.true; - SigninRequest.isOidc("id_token token").should.be.true; - SigninRequest.isOidc("token id_token").should.be.true; - SigninRequest.isOidc("token").should.be.false; - }); - }); - - describe("isOAuth", function() { - it("should indicate if response_type is oauth", function() { - SigninRequest.isOAuth("token").should.be.true; - SigninRequest.isOAuth("id_token token").should.be.true; - SigninRequest.isOAuth("token id_token").should.be.true; - SigninRequest.isOAuth("id_token").should.be.false; - }); - }); - - describe("isCode", function() { - it("should indicate if response_type is code", function() { - SigninRequest.isCode("code").should.be.true; - SigninRequest.isCode("id_token code").should.be.true; - SigninRequest.isCode("code id_token").should.be.true; - SigninRequest.isCode("id_token token code").should.be.true; - SigninRequest.isCode("id_token code token").should.be.true; - SigninRequest.isCode("code id_token token").should.be.true; - - SigninRequest.isCode("id_token token").should.be.false; - SigninRequest.isCode("token id_token").should.be.false; - }); - }); - -}); diff --git a/test/unit/SigninRequest.test.ts b/test/unit/SigninRequest.test.ts new file mode 100644 index 000000000..e2db60ebe --- /dev/null +++ b/test/unit/SigninRequest.test.ts @@ -0,0 +1,367 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { SigninRequest } from '../../src/SigninRequest'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("SigninRequest", () => { + + let subject: SigninRequest; + let settings: any; + + beforeEach(() => { + settings = { + url: "http://sts/signin", + client_id: "client", + redirect_uri: "http://app", + response_type: "id_token", + scope: "openid", + authority : "op", + data: {data: "test"} + }; + subject = new SigninRequest(settings); + }); + + describe("constructor", () => { + + it("should require a url param", () => { + // arrange + delete settings.url; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('url'); + } + }); + + it("should require a client_id param", () => { + // arrange + delete settings.client_id; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('client_id'); + } + }); + + it("should require a redirect_uri param", () => { + // arrange + delete settings.redirect_uri; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('redirect_uri'); + } + }); + + it("should require a response_type param", () => { + // arrange + delete settings.response_type; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('response_type'); + } + }); + + it("should require a scope param", () => { + // arrange + delete settings.scope; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('scope'); + } + }); + + it("should require a authority param", () => { + // arrange + delete settings.authority; + + // act + try { + new SigninRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('authority'); + } + }); + }); + + describe("url", () => { + + it("should include url", () => { + // assert + expect(subject.url.indexOf("http://sts/signin")).toEqual(0); + }); + + it("should include client_id", () => { + // assert + expect(subject.url).toContain("client_id=client"); + }); + + it("should include redirect_uri", () => { + // assert + expect(subject.url).toContain("redirect_uri=" + encodeURIComponent("http://app")); + }); + + it("should include response_type", () => { + // assert + expect(subject.url).toContain("response_type=id_token"); + }); + + it("should include scope", () => { + // assert + expect(subject.url).toContain("scope=openid"); + }); + + it("should include state", () => { + // assert + expect(subject.url).toContain("state=" + subject.state.id); + }); + + it("should include prompt", () => { + // arrange + settings.prompt = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("prompt=foo"); + }); + + it("should include display", () => { + // arrange + settings.display = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("display=foo"); + }); + + it("should include max_age", () => { + // arrange + settings.max_age = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("max_age=foo"); + }); + + it("should include ui_locales", () => { + // arrange + settings.ui_locales = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("ui_locales=foo"); + }); + + it("should include id_token_hint", () => { + // arrange + settings.id_token_hint = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("id_token_hint=foo"); + }); + + it("should include login_hint", () => { + // arrange + settings.login_hint = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("login_hint=foo"); + }); + + it("should include acr_values", () => { + // arrange + settings.acr_values = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("acr_values=foo"); + }); + + it("should include resource", () => { + // arrange + settings.resource = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("resource=foo"); + }); + + it("should include response_mode", () => { + // arrange + settings.response_mode = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("response_mode=foo"); + }); + + it("should include request", () => { + // arrange + settings.request = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("request=foo"); + }); + + it("should include request_uri", () => { + // arrange + settings.request_uri = "foo"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("request_uri=foo"); + }); + + it("should include extra query params", () => { + // arrange + settings.extraQueryParams = { + 'hd': 'domain.com', + 'foo': 'bar' + }; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain('hd=domain.com&foo=bar'); + }); + + it("should store extra token params in state", () => { + // arrange + settings.extraTokenParams = { + 'resourceServer': 'abc', + }; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.state.extraTokenParams).toEqual({ + 'resourceServer': 'abc' + }); + }); + + it("should include code flow params", () => { + // arrange + settings.response_type = "code"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("code_challenge="); + expect(subject.url).toContain("code_challenge_method=S256"); + }); + + it("should include hybrid flow params", () => { + // arrange + settings.response_type = "code id_token"; + + // act + subject = new SigninRequest(settings); + + // assert + expect(subject.url).toContain("nonce="); + expect(subject.url).toContain("code_challenge="); + expect(subject.url).toContain("code_challenge_method=S256"); + }); + }); + + describe("isOidc", () => { + it("should indicate if response_type is oidc", () => { + // assert + expect(SigninRequest.isOidc("id_token")).toEqual(true); + expect(SigninRequest.isOidc("id_token token")).toEqual(true); + expect(SigninRequest.isOidc("token id_token")).toEqual(true); + + expect(SigninRequest.isOidc("token")).toEqual(false); + }); + }); + + describe("isOAuth", () => { + it("should indicate if response_type is oauth", () => { + // assert + expect(SigninRequest.isOAuth("token")).toEqual(true); + expect(SigninRequest.isOAuth("id_token token")).toEqual(true); + expect(SigninRequest.isOAuth("token id_token")).toEqual(true); + + expect(SigninRequest.isOAuth("id_token")).toEqual(false); + }); + }); + + describe("isCode", () => { + it("should indicate if response_type is code", () => { + // assert + expect(SigninRequest.isCode("code")).toEqual(true); + expect(SigninRequest.isCode("id_token code")).toEqual(true); + expect(SigninRequest.isCode("code id_token")).toEqual(true); + expect(SigninRequest.isCode("id_token token code")).toEqual(true); + expect(SigninRequest.isCode("id_token code token")).toEqual(true); + expect(SigninRequest.isCode("code id_token token")).toEqual(true); + + expect(SigninRequest.isCode("id_token token")).toEqual(false); + expect(SigninRequest.isCode("token id_token")).toEqual(false); + }); + }); +}); diff --git a/test/unit/SigninResponse.spec.js b/test/unit/SigninResponse.spec.js deleted file mode 100644 index 31df265ce..000000000 --- a/test/unit/SigninResponse.spec.js +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { SigninResponse } from '../../src/SigninResponse'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; -let expect = chai.expect; - -describe("SigninResponse", function () { - - describe("constructor", function () { - - it("should read error", function () { - let subject = new SigninResponse("error=foo"); - subject.error.should.equal("foo"); - }); - - it("should read error_description", function () { - let subject = new SigninResponse("error_description=foo"); - subject.error_description.should.equal("foo"); - }); - - it("should read error_uri", function () { - let subject = new SigninResponse("error_uri=foo"); - subject.error_uri.should.equal("foo"); - }); - - it("should read state", function () { - let subject = new SigninResponse("state=foo"); - subject.state.should.equal("foo"); - }); - - it("should read code", function () { - let subject = new SigninResponse("code=foo"); - subject.code.should.equal("foo"); - }); - - it("should read id_token", function () { - let subject = new SigninResponse("id_token=foo"); - subject.id_token.should.equal("foo"); - }); - - it("should read session_state", function () { - let subject = new SigninResponse("session_state=foo"); - subject.session_state.should.equal("foo"); - }); - - it("should read access_token", function () { - let subject = new SigninResponse("access_token=foo"); - subject.access_token.should.equal("foo"); - }); - - it("should read token_type", function () { - let subject = new SigninResponse("token_type=foo"); - subject.token_type.should.equal("foo"); - }); - - it("should read scope", function () { - let subject = new SigninResponse("scope=foo"); - subject.scope.should.equal("foo"); - }); - - it("should read expires_in", function () { - let subject = new SigninResponse("expires_in=10"); - subject.expires_in.should.equal(10); - }); - - it("should calculate expires_at", function () { - let subject = new SigninResponse("expires_in=10"); - subject.expires_at.should.equal(parseInt((Date.now() / 1000) + 10)); - }); - - it("should not read invalid expires_in", function () { - let subject = new SigninResponse("expires_in=foo"); - expect(subject.expires_in).to.be.undefined; - expect(subject.expires_at).to.be.undefined; - - subject = new SigninResponse("expires_in=-10"); - expect(subject.expires_in).to.be.undefined; - expect(subject.expires_at).to.be.undefined; - }); - - }); - - describe("scopes", function () { - it("should return list of scope", function () { - let subject = new SigninResponse("scope=foo"); - subject.scopes.should.deep.equal(["foo"]); - - subject = new SigninResponse("scope=foo%20bar"); - subject.scopes.should.deep.equal(["foo", "bar"]); - - subject = new SigninResponse("scope=foo%20bar%20baz"); - subject.scopes.should.deep.equal(["foo", "bar", "baz"]); - }); - }); - - describe("expires_in", function () { - it("should calculate how much time left", function () { - var oldNow = Date.now; - Date.now = function () { - return 1000 * 1000; // ms - } - let subject = new SigninResponse("expires_in=100"); - subject.expires_in.should.equal(100); - - Date.now = function () { - return 1050 * 1000; // ms - } - subject.expires_in.should.equal(50); - Date.now = oldNow; - }); - }); - - describe("expired", function () { - it("should calculate how much time left", function () { - var oldNow = Date.now; - Date.now = function () { - return 1000 * 1000; // ms - } - let subject = new SigninResponse("expires_in=100"); - subject.expired.should.be.false; - - Date.now = function () { - return 1100 * 1000; // ms - } - subject.expired.should.be.true; - Date.now = oldNow; - }); - }); - - describe("isOpenIdConnect", function () { - it("should detect openid scope", function () { - let subject = new SigninResponse("scope=foo%20openid%20bar"); - subject.isOpenIdConnect.should.be.true; - - subject = new SigninResponse("scope=openid%20foo%20bar"); - subject.isOpenIdConnect.should.be.true; - - subject = new SigninResponse("scope=foo%20bar%20openid"); - subject.isOpenIdConnect.should.be.true; - - subject = new SigninResponse("scope=foo%20bar"); - subject.isOpenIdConnect.should.be.false; - }); - }); - -}); diff --git a/test/unit/SigninResponse.test.ts b/test/unit/SigninResponse.test.ts new file mode 100644 index 000000000..b492a0f6d --- /dev/null +++ b/test/unit/SigninResponse.test.ts @@ -0,0 +1,217 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { SigninResponse } from '../../src/SigninResponse'; + +describe("SigninResponse", () => { + + describe("constructor", () => { + + it("should read error", () => { + // act + let subject = new SigninResponse("error=foo"); + + // assert + expect(subject.error).toEqual("foo"); + }); + + it("should read error_description", () => { + // act + let subject = new SigninResponse("error_description=foo"); + + // assert + expect(subject.error_description).toEqual("foo"); + }); + + it("should read error_uri", () => { + // act + let subject = new SigninResponse("error_uri=foo"); + + // assert + expect(subject.error_uri).toEqual("foo"); + }); + + it("should read state", () => { + // act + let subject = new SigninResponse("state=foo"); + + // assert + expect(subject.state).toEqual("foo"); + }); + + it("should read code", () => { + // act + let subject = new SigninResponse("code=foo"); + + // assert + expect(subject.code).toEqual("foo"); + }); + + it("should read id_token", () => { + // act + let subject = new SigninResponse("id_token=foo"); + + // assert + expect(subject.id_token).toEqual("foo"); + }); + + it("should read session_state", () => { + // act + let subject = new SigninResponse("session_state=foo"); + + // assert + expect(subject.session_state).toEqual("foo"); + }); + + it("should read access_token", () => { + // act + let subject = new SigninResponse("access_token=foo"); + + // assert + expect(subject.access_token).toEqual("foo"); + }); + + it("should read token_type", () => { + // act + let subject = new SigninResponse("token_type=foo"); + + // assert + expect(subject.token_type).toEqual("foo"); + }); + + it("should read scope", () => { + // act + let subject = new SigninResponse("scope=foo"); + + // assert + expect(subject.scope).toEqual("foo"); + }); + + it("should read expires_in", () => { + // act + let subject = new SigninResponse("expires_in=10"); + + // assert + expect(subject.expires_in).toEqual(10); + }); + + it("should calculate expires_at", () => { + // act + let subject = new SigninResponse("expires_in=10"); + + // assert + expect(subject.expires_at).toEqual(Math.floor((Date.now() / 1000) + 10)); + }); + + it("should not read invalid expires_in", () => { + // act + let subject = new SigninResponse("expires_in=foo"); + + // assert + expect(subject.expires_in).toBeUndefined(); + expect(subject.expires_at).toBeUndefined(); + + // act + subject = new SigninResponse("expires_in=-10"); + + // assert + expect(subject.expires_in).toBeUndefined(); + expect(subject.expires_at).toBeUndefined(); + }); + + }); + + describe("scopes", () => { + it("should return list of scope", () => { + // act + let subject = new SigninResponse("scope=foo"); + + // assert + expect(subject.scopes).toEqual(["foo"]); + + subject = new SigninResponse("scope=foo%20bar"); + + // assert + expect(subject.scopes).toEqual(["foo", "bar"]); + + subject = new SigninResponse("scope=foo%20bar%20baz"); + + // assert + expect(subject.scopes).toEqual(["foo", "bar", "baz"]); + }); + }); + + describe("expires_in", () => { + it("should calculate how much time left", () => { + var oldNow = Date.now; + Date.now = () => { + return 1000 * 1000; // ms + } + + // act + let subject = new SigninResponse("expires_in=100"); + + // assert + expect(subject.expires_in).toEqual(100); + + Date.now = () => { + return 1050 * 1000; // ms + } + + // assert + expect(subject.expires_in).toEqual(50); + Date.now = oldNow; + }); + }); + + describe("expired", () => { + it("should calculate how much time left", () => { + var oldNow = Date.now; + Date.now = () => { + return 1000 * 1000; // ms + } + + // act + let subject = new SigninResponse("expires_in=100"); + + // assert + expect(subject.expired).toEqual(false); + + Date.now = () => { + return 1100 * 1000; // ms + } + + // assert + expect(subject.expired).toEqual(true); + Date.now = oldNow; + }); + }); + + describe("isOpenIdConnect", () => { + it("should detect openid scope", () => { + // act + let subject = new SigninResponse("scope=foo%20openid%20bar"); + + // assert + expect(subject.isOpenIdConnect).toEqual(true); + + // act + subject = new SigninResponse("scope=openid%20foo%20bar"); + + // assert + expect(subject.isOpenIdConnect).toEqual(true); + + // act + subject = new SigninResponse("scope=foo%20bar%20openid"); + + // assert + expect(subject.isOpenIdConnect).toEqual(true); + + // act + subject = new SigninResponse("scope=foo%20bar"); + + // assert + expect(subject.isOpenIdConnect).toEqual(false); + }); + }); +}); diff --git a/test/unit/SigninState.spec.js b/test/unit/SigninState.spec.js deleted file mode 100644 index ed2583bfa..000000000 --- a/test/unit/SigninState.spec.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { SigninState } from '../../src/SigninState'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("SigninState", function() { - - beforeEach(function(){ - Log.level = Log.NONE; - Log.logger = console; - }); - - describe("constructor", function() { - - it("should call base ctor", function() { - var subject = new SigninState({ id: 5, created:6, data:7 }); - subject.id.should.be.equal(5); - subject.created.should.be.equal(6); - subject.data.should.be.equal(7); - }); - - it("should accept nonce", function() { - var subject = new SigninState({ nonce: 5 }); - subject.nonce.should.be.equal(5); - }); - - it("should generate nonce", function() { - var subject = new SigninState({ nonce: true }); - subject.nonce.should.be.ok; - }); - - it("should accept redirect_uri", function() { - var subject = new SigninState({ redirect_uri: "http://cb" }); - subject.redirect_uri.should.be.equal("http://cb"); - }); - - it("should accept code_verifier", function() { - var subject = new SigninState({ code_verifier: 5 }); - subject.code_verifier.should.be.equal(5); - }); - - it("should generate code_verifier", function() { - var subject = new SigninState({ code_verifier: true }); - subject.code_verifier.should.be.ok; - }); - - it("should generate code_challenge", function() { - var subject = new SigninState({ code_verifier: true }); - subject.code_challenge.should.be.ok; - }); - - it("should accept client_id", function() { - var subject = new SigninState({ client_id: "client" }); - subject.client_id.should.be.equal("client"); - }); - - it("should accept authority", function() { - var subject = new SigninState({ authority: "test" }); - subject.authority.should.be.equal("test"); - }); - - it("should accept request_type", function() { - var subject = new SigninState({ request_type: 'xoxo' }); - subject.request_type.should.be.equal('xoxo'); - }); - - it("should accept extraTokenParams", function() { - var subject = new SigninState({ extraTokenParams: { 'resourceServer' : 'abc' } }); - assert.deepEqual(subject.extraTokenParams, { 'resourceServer' : 'abc' }); - }); - }); - - it("can serialize and then deserialize", function() { - var subject1 = new SigninState({ nonce: true, data: { foo: "test" }, created: 1000, client_id:"client", authority:"authority", redirect_uri:"http://cb", code_verifier:true, request_type:'type' }); - - var storage = subject1.toStorageString(); - var subject2 = SigninState.fromStorageString(storage); - - subject2.should.be.deep.equal(subject1); - }); - -}); diff --git a/test/unit/SigninState.test.ts b/test/unit/SigninState.test.ts new file mode 100644 index 000000000..42f28d49e --- /dev/null +++ b/test/unit/SigninState.test.ts @@ -0,0 +1,144 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { SigninState } from '../../src/SigninState'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn().mockImplementation(() => "hextob64u"), + b64tohex: jest.fn() + }; +}); + +describe("SigninState", () => { + + beforeEach(() =>{ + Log.level = Log.NONE; + Log.logger = console; + }); + + describe("constructor", () => { + + it("should call base ctor", () => { + // act + var subject = new SigninState({ id: 5, created:6, data:7 }); + + // assert + expect(subject.id).toEqual(5); + expect(subject.created).toEqual(6); + expect(subject.data).toEqual(7); + }); + + it("should accept nonce", () => { + // act + var subject = new SigninState({ nonce: 5 }); + + // assert + expect(subject.nonce).toEqual(5); + }); + + it("should generate nonce", () => { + // act + var subject = new SigninState({ nonce: true }); + + // assert + expect(subject.nonce).toBeDefined(); + }); + + it("should accept redirect_uri", () => { + // act + var subject = new SigninState({ redirect_uri: "http://cb" }); + + // assert + expect(subject.redirect_uri).toEqual("http://cb"); + }); + + it("should accept code_verifier", () => { + // act + var subject = new SigninState({ code_verifier: 5 }); + + // assert + expect(subject.code_verifier).toEqual(5); + }); + + it("should generate code_verifier", () => { + // act + var subject = new SigninState({ code_verifier: true }); + + // assert + expect(subject.code_verifier).toBeDefined(); + }); + + it("should generate code_challenge", () => { + // arrange + + + // act + var subject = new SigninState({ code_verifier: true }); + + // assert + expect(subject.code_challenge).toBeDefined(); + }); + + it("should accept client_id", () => { + // act + var subject = new SigninState({ client_id: "client" }); + + // assert + expect(subject.client_id).toEqual("client"); + }); + + it("should accept authority", () => { + // act + var subject = new SigninState({ authority: "test" }); + + // assert + expect(subject.authority).toEqual("test"); + }); + + it("should accept request_type", () => { + // act + var subject = new SigninState({ request_type: 'xoxo' }); + + // assert + expect(subject.request_type).toEqual('xoxo'); + }); + + it("should accept extraTokenParams", () => { + // act + var subject = new SigninState({ + extraTokenParams: { 'resourceServer' : 'abc' } + }); + + // assert + expect(subject.extraTokenParams).toEqual({ 'resourceServer' : 'abc' }); + }); + }); + + it("can serialize and then deserialize", () => { + // arrange + var subject1 = new SigninState({ + nonce: true, + data: { foo: "test" }, + created: 1000, + client_id: "client", + authority: "authority", + redirect_uri: "http://cb", + code_verifier: true, + request_type: 'type' + }); + + // act + var storage = subject1.toStorageString(); + var subject2 = SigninState.fromStorageString(storage); + + // assert + expect(subject2).toEqual(subject1); + }); +}); diff --git a/test/unit/SignoutRequest.spec.js b/test/unit/SignoutRequest.spec.js deleted file mode 100644 index bbd5391b8..000000000 --- a/test/unit/SignoutRequest.spec.js +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { SignoutRequest } from '../../src/SignoutRequest'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("SignoutRequest", function() { - - let subject; - let settings; - - beforeEach(function() { - settings = { - url: "http://sts/signout", - id_token_hint: "hint", - post_logout_redirect_uri: "loggedout", - data: { data: "test" } - }; - subject = new SignoutRequest(settings); - }); - - describe("constructor", function() { - - it("should require a url param", function() { - try { - delete settings.url; - new SignoutRequest(settings); - } - catch (e) { - e.message.should.contain('url'); - return; - } - assert.fail(); - }); - - }); - - describe("url", function() { - - it("should include url", function() { - subject.url.indexOf("http://sts/signout").should.equal(0); - }); - - it("should include id_token_hint", function() { - subject.url.should.contain("id_token_hint=hint"); - }); - - it("should include post_logout_redirect_uri if id_token_hint also provided", function() { - subject.url.should.contain("post_logout_redirect_uri=loggedout"); - }); - - it("should include post_logout_redirect_uri if no id_token_hint provided", function() { - - delete settings.id_token_hint; - subject = new SignoutRequest(settings); - - subject.url.should.contain("post_logout_redirect_uri=loggedout"); - }); - - it("should include state if post_logout_redirect_uri provided", function() { - subject.url.should.contain("state=" + subject.state.id); - }); - - it("should not include state if no post_logout_redirect_uri provided", function() { - delete settings.post_logout_redirect_uri; - subject = new SignoutRequest(settings); - subject.url.should.not.contain("state="); - }); - - - it("should include id_token_hint, post_logout_redirect_uri, and state", function() { - var url = subject.url; - url.indexOf('http://sts/signout?').should.equal(0); - url.should.contain("id_token_hint=hint"); - url.should.contain("post_logout_redirect_uri=loggedout"); - url.should.contain("state=" + subject.state.id); - }); - - it("should include extra query params", function() { - settings.extraQueryParams = { - 'TargetResource': 'logouturl.com', - 'InErrorResource': 'errorurl.com' - }; - subject = new SignoutRequest(settings); - subject.url.should.contain('TargetResource=logouturl.com&InErrorResource=errorurl.com'); - }); - - }); - -}); diff --git a/test/unit/SignoutRequest.test.ts b/test/unit/SignoutRequest.test.ts new file mode 100644 index 000000000..1c710d653 --- /dev/null +++ b/test/unit/SignoutRequest.test.ts @@ -0,0 +1,109 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { SignoutRequest } from '../../src/SignoutRequest'; + +describe("SignoutRequest", () => { + + let subject: SignoutRequest; + let settings: any; + + beforeEach(() => { + settings = { + url: "http://sts/signout", + id_token_hint: "hint", + post_logout_redirect_uri: "loggedout", + data: { data: "test" } + }; + subject = new SignoutRequest(settings); + }); + + describe("constructor", () => { + + it("should require a url param", () => { + // arrange + delete settings.url; + + // act + try { + new SignoutRequest(settings); + fail("should not come here"); + } + catch (e) { + expect(e.message).toContain('url'); + } + }); + }); + + describe("url", () => { + + it("should include url", () => { + // assert + expect(subject.url.indexOf("http://sts/signout")).toEqual(0); + }); + + it("should include id_token_hint", () => { + // assert + expect(subject.url).toContain("id_token_hint=hint"); + }); + + it("should include post_logout_redirect_uri if id_token_hint also provided", () => { + // assert + expect(subject.url).toContain("post_logout_redirect_uri=loggedout"); + }); + + it("should include post_logout_redirect_uri if no id_token_hint provided", () => { + // arrange + delete settings.id_token_hint; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.url).toContain("post_logout_redirect_uri=loggedout"); + }); + + it("should include state if post_logout_redirect_uri provided", () => { + // assert + expect(subject.state).toBeDefined(); + expect(subject.url).toContain("state=" + subject.state!.id); + }); + + it("should not include state if no post_logout_redirect_uri provided", () => { + // arrange + delete settings.post_logout_redirect_uri; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.url).not.toContain("state="); + }); + + + it("should include id_token_hint, post_logout_redirect_uri, and state", () => { + // assert + const url = subject.url; + expect(url.indexOf('http://sts/signout?')).toEqual(0); + expect(url).toContain("id_token_hint=hint"); + expect(url).toContain("post_logout_redirect_uri=loggedout"); + expect(subject.state).toBeDefined(); + expect(url).toContain("state=" + subject.state!.id); + }); + + it("should include extra query params", () => { + // arrange + settings.extraQueryParams = { + 'TargetResource': 'logouturl.com', + 'InErrorResource': 'errorurl.com' + }; + + // act + subject = new SignoutRequest(settings); + + // assert + expect(subject.url).toContain('TargetResource=logouturl.com&InErrorResource=errorurl.com'); + }); + + }); +}); diff --git a/test/unit/SignoutResponse.spec.js b/test/unit/SignoutResponse.spec.js deleted file mode 100644 index 10f0ea4b6..000000000 --- a/test/unit/SignoutResponse.spec.js +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { SignoutResponse } from '../../src/SignoutResponse'; -import { ErrorResponse } from '../../src/ErrorResponse'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; -let expect = chai.expect; - -describe("SignoutResponse", function() { - - describe("constructor", function() { - - it("should read error", function () { - let subject = new SignoutResponse("error=foo"); - subject.error.should.equal("foo"); - }); - - it("should read error_description", function () { - let subject = new SignoutResponse("error_description=foo"); - subject.error_description.should.equal("foo"); - }); - - it("should read error_uri", function () { - let subject = new SignoutResponse("error_uri=foo"); - subject.error_uri.should.equal("foo"); - }); - - it("should read state", function() { - let subject = new SignoutResponse("state=foo"); - subject.state.should.equal("foo"); - }); - - }); - -}); diff --git a/test/unit/SignoutResponse.test.ts b/test/unit/SignoutResponse.test.ts new file mode 100644 index 000000000..bc6538da7 --- /dev/null +++ b/test/unit/SignoutResponse.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { SignoutResponse } from '../../src/SignoutResponse'; + +describe("SignoutResponse", () => { + + describe("constructor", () => { + + it("should read error", () => { + // act + let subject = new SignoutResponse("error=foo"); + + // assert + expect(subject.error).toEqual("foo"); + }); + + it("should read error_description", () => { + // act + let subject = new SignoutResponse("error_description=foo"); + + // assert + expect(subject.error_description).toEqual("foo"); + }); + + it("should read error_uri", () => { + // act + let subject = new SignoutResponse("error_uri=foo"); + + // assert + expect(subject.error_uri).toEqual("foo"); + }); + + it("should read state", () => { + // act + let subject = new SignoutResponse("state=foo"); + + // assert + expect(subject.state).toEqual("foo"); + }); + }); +}); diff --git a/test/unit/State.spec.js b/test/unit/State.test.ts similarity index 59% rename from test/unit/State.spec.js rename to test/unit/State.test.ts index d87b6fb30..a4a2b675b 100644 --- a/test/unit/State.spec.js +++ b/test/unit/State.test.ts @@ -7,74 +7,98 @@ import { State } from '../../src/State'; import { InMemoryWebStorage } from '../../src/InMemoryWebStorage'; import { WebStorageStateStore } from '../../src/WebStorageStateStore'; -import chai from 'chai'; -chai.should(); -let assert = chai.assert; +describe("State", () => { -describe("State", function() { - - beforeEach(function(){ + beforeEach(() =>{ Log.level = Log.NONE; Log.logger = console; }); - describe("constructor", function() { + describe("constructor", () => { - it("should generate id", function() { + it("should generate id", () => { + // act var subject = new State(); - subject.id.should.be.ok; + + // assert + expect(subject.id).toBeDefined(); }); - it("should accept id", function() { + it("should accept id", () => { + // act var subject = new State({ id: 5 }); - subject.id.should.be.equal(5); + + // assert + expect(subject.id).toEqual(5); }); - it("should accept data", function() { + it("should accept data", () => { + // act var subject = new State({ data: "test" }); - subject.data.should.be.equal("test"); + + // assert + expect(subject.data).toEqual("test"); }); - it("should accept data as objects", function() { + it("should accept data as objects", () => { + // act var subject = new State({ data: { foo: "test" } }); - subject.data.should.be.deep.equal({ foo: "test" }); + + // assert + expect(subject.data).toEqual({ foo: "test" }); }); - it("should accept created", function() { + it("should accept created", () => { + // act var subject = new State({ created: 1000 }); - subject.created.should.be.equal(1000); + + // assert + expect(subject.created).toEqual(1000); }); - it("should use date.now for created", function() { + it("should use date.now for created", () => { + // arrange var oldNow = Date.now; - Date.now = function() { + Date.now = () => { return 123 * 1000; // ms }; + + // act var subject = new State(); - subject.created.should.be.equal(123); + + // assert + expect(subject.created).toEqual(123); Date.now = oldNow; }); - it("should accept request_type", function() { + + it("should accept request_type", () => { + // act var subject = new State({ request_type: 'xoxo' }); - subject.request_type.should.be.equal('xoxo'); + + // assert + expect(subject.request_type).toEqual('xoxo'); }); }); - it("can serialize and then deserialize", function() { + it("can serialize and then deserialize", () => { + // arrange var subject1 = new State({ data: { foo: "test" }, created: 1000, request_type:'type' }); + // act var storage = subject1.toStorageString(); var subject2 = State.fromStorageString(storage); - subject2.should.be.deep.equal(subject1); - }); - describe("clearStaleState", function() { + // assert + expect(subject2).toEqual(subject1); + }); - it("should remove old state entries", function(done) { + describe("clearStaleState", () => { + it("should remove old state entries", async () => { + // arrange let oldNow = Date.now; - Date.now = function() { + Date.now = () => { return 200 * 1000; // ms }; @@ -100,20 +124,14 @@ describe("State", function() { inMemStore.setItem(prefix + s5.id, s5.toStorageString()); inMemStore.setItem("junk5", "junk"); - State.clearStaleState(store, 100).then(() => { - Log.debug("clearStaleState done"); - - inMemStore.length.should.equal(8); - inMemStore.getItem(prefix + "s4").should.be.ok; - inMemStore.getItem(prefix + "s5").should.be.ok; - - Date.now = oldNow; - done(); - }); + // act + await State.clearStaleState(store, 100); + // assert + expect(inMemStore.length).toEqual(8); + expect(inMemStore.getItem(prefix + "s4")).toBeDefined(); + expect(inMemStore.getItem(prefix + "s5")).toBeDefined(); + Date.now = oldNow; }); - }); - - }); diff --git a/test/unit/StubJsonService.js b/test/unit/StubJsonService.ts similarity index 81% rename from test/unit/StubJsonService.js rename to test/unit/StubJsonService.ts index 51e79a1d3..8eda70dbb 100644 --- a/test/unit/StubJsonService.js +++ b/test/unit/StubJsonService.ts @@ -4,7 +4,11 @@ import { Log } from '../../src/Log'; export class StubJsonService { - getJson(url, token) { + url: any; + token: any; + result: any; + + getJson(url: string, token?: string) { Log.debug("StubJsonService.getJson", this.result); this.url = url; diff --git a/test/unit/StubMetadataService.js b/test/unit/StubMetadataService.ts similarity index 53% rename from test/unit/StubMetadataService.js rename to test/unit/StubMetadataService.ts index 2d2e90b1b..9252a27a1 100644 --- a/test/unit/StubMetadataService.js +++ b/test/unit/StubMetadataService.ts @@ -1,24 +1,31 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. -export class StubMetadataService{ - resetSigningKeys(){ } - getMetadata(){ +export class StubMetadataService { + getMetadataResult: any; + getIssuerResult: any; + getAuthorizationEndpointResult: any; + getEndSessionEndpointResult: any; + userInfoEndpointResult: any; + getSigningKeysResult: any; + + resetSigningKeys() { } + getMetadata() { return this.getMetadataResult; } - getIssuer(){ + getIssuer() { return this.getIssuerResult; } - getAuthorizationEndpoint(){ + getAuthorizationEndpoint() { return this.getAuthorizationEndpointResult; } - getEndSessionEndpoint(){ + getEndSessionEndpoint() { return this.getEndSessionEndpointResult; } - getUserInfoEndpoint(){ + getUserInfoEndpoint() { return this.userInfoEndpointResult; } - getSigningKeys(){ + getSigningKeys() { return this.getSigningKeysResult; } } diff --git a/test/unit/StubResponseValidator.js b/test/unit/StubResponseValidator.ts similarity index 50% rename from test/unit/StubResponseValidator.js rename to test/unit/StubResponseValidator.ts index 7e8ad9e9a..32a03e859 100644 --- a/test/unit/StubResponseValidator.js +++ b/test/unit/StubResponseValidator.ts @@ -1,20 +1,27 @@ // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. +import { SigninResponse } from "../../src/SigninResponse"; +import { SignoutResponse } from "../../src/SignoutResponse"; +import { SigninState } from "../../src/SigninState"; +import { State } from "../../src/State"; + export class StubResponseValidator { - validateSigninResponse(state, response) { + signinState: any; + signinResponse: any; + + signoutState: any; + signoutResponse: any; + validateSigninResponse(state: SigninState, response: SigninResponse) { this.signinState = state; this.signinResponse = response; - return Promise.resolve(response); } - validateSignoutResponse(state, response) { - + validateSignoutResponse(state: State, response: SignoutResponse) { this.signoutState = state; this.signoutResponse = response; - return Promise.resolve(response); } } diff --git a/test/unit/StubSilentRenewService.js b/test/unit/StubSilentRenewService.js deleted file mode 100644 index e85b85d3d..000000000 --- a/test/unit/StubSilentRenewService.js +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -export class StubSilentRenewService { - start(){ - } - stop(){ - } -} diff --git a/test/unit/StubStateStore.js b/test/unit/StubStateStore.ts similarity index 77% rename from test/unit/StubStateStore.js rename to test/unit/StubStateStore.ts index db138a362..aca8b98a7 100644 --- a/test/unit/StubStateStore.js +++ b/test/unit/StubStateStore.ts @@ -2,7 +2,10 @@ // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. export class StubStateStore { - set(key, value) { + error: any; + item: any; + + set(_key: string, value: string) { if (this.error) { return Promise.reject(new Error(this.error)); } @@ -10,27 +13,27 @@ export class StubStateStore { return Promise.resolve(); } - get(key) { + get(_key: string) { if (this.error) { return Promise.reject(new Error(this.error)); } return Promise.resolve(this.item); } - remove(key) { + remove(_key: string) { if (this.error) { return Promise.reject(new Error(this.error)); } return Promise.resolve(this.item); } - getAllKeys(){ - if (this.item){ + getAllKeys() { + if (this.item) { return Promise.resolve(["key"]); } - return Promise.resolve([]); + return Promise.resolve([]); } - get length(){ + get length() { return this.item ? 1 : 0; } } diff --git a/test/unit/StubTokenRevocationClient.js b/test/unit/StubTokenRevocationClient.js deleted file mode 100644 index e14595837..000000000 --- a/test/unit/StubTokenRevocationClient.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -export class StubTokenRevocationClient { - revoke(accessToken, required) { - this.accessToken = accessToken; - if (this.error) { - return Promise.reject(new Error(this.error)); - } - return Promise.resolve(); - } -} diff --git a/test/unit/Timer.spec.js b/test/unit/Timer.spec.js deleted file mode 100644 index 5789ac7aa..000000000 --- a/test/unit/Timer.spec.js +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Timer } from '../../src/Timer'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -class StubWindowTimer { - - constructor() { - this.clearTimeoutWasCalled = false; - } - - setInterval(cb, duration) { - this.callback = cb; - this.duration = duration; - return 5; - } - - clearInterval() { - this.clearTimeoutWasCalled = true; - } -} - -describe("Timer", function () { - - let subject; - let stubWindowTimer; - let now = Date.now() / 1000; - - beforeEach(function () { - stubWindowTimer = new StubWindowTimer(); - subject = new Timer("test name", stubWindowTimer, () => now); - }); - - describe("init", function () { - - it("should setup a timer", function () { - subject.init(10); - stubWindowTimer.callback.should.be.ok; - }); - - it("should use 1 second if duration is too low", function () { - subject.init(0); - stubWindowTimer.duration.should.equal(1000); - subject.init(-1); - stubWindowTimer.duration.should.equal(1000); - subject.init(-5); - stubWindowTimer.duration.should.equal(1000); - }); - - it("should use duration if less than default", function () { - subject.init(2); - stubWindowTimer.duration.should.equal(2000); - subject.init(3); - stubWindowTimer.duration.should.equal(3000); - }); - - it("should cancel previous timer if new time is not the same", function () { - subject.init(10); - stubWindowTimer.clearTimeoutWasCalled.should.be.false; - - now = now + 1; - subject.init(10); - - stubWindowTimer.clearTimeoutWasCalled.should.be.true; - }); - - it("should not cancel previous timer if new time is same", function () { - subject.init(10); - stubWindowTimer.clearTimeoutWasCalled.should.be.false; - - subject.init(10); - stubWindowTimer.clearTimeoutWasCalled.should.be.false; - }); - }); - - describe("_callback", function () { - - it("should fire when timer expires", function () { - var cb = function () { - cb.wasCalled = true; - }; - cb.wasCalled = false; - subject.addHandler(cb); - - subject._nowFunc = () => 100; - subject.init(10); - - subject._nowFunc = () => 109; - stubWindowTimer.callback(); - cb.wasCalled.should.be.false; - - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - cb.wasCalled.should.be.true; - }); - - - it("should fire if timer late", function () { - var cb = function () { - cb.wasCalled = true; - }; - cb.wasCalled = false; - subject.addHandler(cb); - - subject._nowFunc = () => 100; - subject.init(10); - - subject._nowFunc = () => 109; - stubWindowTimer.callback(); - cb.wasCalled.should.be.false; - - subject._nowFunc = () => 111; - stubWindowTimer.callback(); - cb.wasCalled.should.be.true; - }); - - it("should cancel window timer", function () { - subject._nowFunc = () => 100; - subject.init(10); - - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - - stubWindowTimer.clearTimeoutWasCalled.should.be.true; - }); - }); - - describe("cancel", function () { - - it("should cancel timer", function () { - subject.init(10); - stubWindowTimer.clearTimeoutWasCalled.should.be.false; - - subject.cancel(); - - stubWindowTimer.clearTimeoutWasCalled.should.be.true; - }); - - it("should do nothing if no existing timer", function () { - subject.cancel(); - - stubWindowTimer.clearTimeoutWasCalled.should.be.false; - }); - }); - - describe("addHandler", function () { - - it("should allow callback to be invoked", function () { - var cb = function () { - cb.wasCalled = true; - }; - subject.addHandler(cb); - - subject._nowFunc = () => 100; - subject.init(10); - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - - cb.wasCalled.should.be.true; - }); - - it("should allow multiple callbacks", function () { - var count = 0; - var cb = function () { - count++; - }; - subject.addHandler(cb); - subject.addHandler(cb); - subject.addHandler(cb); - subject.addHandler(cb); - - subject._nowFunc = () => 100; - subject.init(10); - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - - count.should.equal(4); - }); - - }); - - describe("removeHandler", function () { - - it("should remove callback from being invoked", function () { - var cb = function () { - cb.wasCalled = true; - }; - cb.wasCalled = false; - - subject._nowFunc = () => 100; - subject.addHandler(cb); - subject.init(10); - subject.removeHandler(cb); - - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - - cb.wasCalled.should.be.false; - }); - - it("should remove individual callback", function () { - var count = 0; - var cb1 = function () { - count++; - }; - var cb2 = function () { - cb2.wasCalled = true; - }; - - subject.addHandler(cb1); - subject.addHandler(cb2); - subject.addHandler(cb1); - - subject._nowFunc = () => 100; - subject.init(10); - subject.removeHandler(cb1); - subject.removeHandler(cb1); - - subject._nowFunc = () => 110; - stubWindowTimer.callback(); - - count.should.equal(0); - cb2.wasCalled.should.be.true; - }); - - }); - -}); diff --git a/test/unit/Timer.test.ts b/test/unit/Timer.test.ts new file mode 100644 index 000000000..66df79003 --- /dev/null +++ b/test/unit/Timer.test.ts @@ -0,0 +1,285 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { IntervalTimer, Timer } from '../../src/Timer'; + +class IntervalTimerMock implements IntervalTimer { + callback: ((...args: any[]) => void); + duration?: number; + + clearTimeoutWasCalled: boolean; + clearHandle: number | null; + + constructor() { + this.callback = () => { fail("should not come here"); }; + this.duration = undefined; + + this.clearTimeoutWasCalled = false; + this.clearHandle = null; + } + + setInterval(cb: (...args: any[]) => void, duration?: number) { + this.callback = cb; + this.duration = duration; + return 5; + } + + clearInterval(handle: number) { + this.clearTimeoutWasCalled = true; + this.clearHandle = handle; + } +} + +describe("Timer", () => { + + let subject: Timer; + let intervalTimerMock: IntervalTimerMock; + let now = Math.floor(Date.now() / 1000); + + beforeEach(() => { + intervalTimerMock = new IntervalTimerMock(); + subject = new Timer("test name", intervalTimerMock, () => now); + }); + + describe("init", () => { + + it("should setup a timer", () => { + // act + subject.init(10); + + // assert + expect(intervalTimerMock.callback).not.toBeNull(); + }); + + it("should use 1 second if duration is too low", () => { + // act + subject.init(0); + + // assert + expect(intervalTimerMock.duration).toEqual(1000); + + // act + subject.init(-1); + + // assert + expect(intervalTimerMock.duration).toEqual(1000); + + // act + subject.init(-5); + + // assert + expect(intervalTimerMock.duration).toEqual(1000); + }); + + it("should use duration if less than default", () => { + // act + subject.init(2); + + // assert + expect(intervalTimerMock.duration).toEqual(2000); + + // act + subject.init(3); + + // assert + expect(intervalTimerMock.duration).toEqual(3000); + }); + + it("should cancel previous timer if new time is not the same", () => { + // act + subject.init(10); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(false); + + // act + now = now + 1; + subject.init(10); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(true); + }); + + it("should not cancel previous timer if new time is same", () => { + // act + subject.init(10); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(false); + + // act + subject.init(10); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(false); + }); + }); + + describe("_callback", () => { + + it("should fire when timer expires", () => { + // arrange + var cb = jest.fn(); + subject.addHandler(cb); + + now = 100; + subject.init(10); + + // assert + expect(intervalTimerMock.callback).not.toBeNull(); + + // act + now = 109; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(0); + + // act + now = 110; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(1); + }); + + it("should fire if timer late", () => { + // arrange + var cb = jest.fn(); + subject.addHandler(cb); + + now = 100; + subject.init(10); + + // assert + expect(intervalTimerMock.callback).not.toBeNull(); + + now = 109; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(0); + + now = 111; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(1); + }); + + it("should cancel window timer", () => { + now = 100; + subject.init(10); + + // assert + expect(intervalTimerMock.callback).not.toBeNull(); + + now = 110; + intervalTimerMock.callback(); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(true); + }); + }); + + describe("cancel", () => { + + it("should cancel timer", () => { + // act + subject.init(10); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(false); + + // act + subject.cancel(); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(true); + }); + + it("should do nothing if no existing timer", () => { + // act + subject.cancel(); + + // assert + expect(intervalTimerMock.clearTimeoutWasCalled).toEqual(false); + }); + }); + + describe("addHandler", () => { + + it("should allow callback to be invoked", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addHandler(cb); + now = 100; + subject.init(10); + now = 110; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalled(); + }); + + it("should allow multiple callbacks", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addHandler(cb); + subject.addHandler(cb); + subject.addHandler(cb); + subject.addHandler(cb); + now = 100; + subject.init(10); + now = 110; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(4); + }); + }); + + describe("removeHandler", () => { + + it("should remove callback from being invoked", () => { + // arrange + var cb = jest.fn(); + now = 100; + subject.addHandler(cb); + subject.init(10); + + // act + subject.removeHandler(cb); + now = 110; + intervalTimerMock.callback(); + + // assert + expect(cb).toBeCalledTimes(0); + }); + + it("should remove individual callback", () => { + // arrange + var cb1 = jest.fn(); + var cb2 = jest.fn(); + subject.addHandler(cb1); + subject.addHandler(cb2); + subject.addHandler(cb1); + + // act + now = 100; + subject.init(10); + subject.removeHandler(cb1); + subject.removeHandler(cb1); + now = 110; + intervalTimerMock.callback(); + + // assert + expect(cb1).toBeCalledTimes(0); + expect(cb2).toBeCalledTimes(1); + }); + }); +}); diff --git a/test/unit/UrlUtility.spec.js b/test/unit/UrlUtility.spec.js deleted file mode 100644 index bc9437672..000000000 --- a/test/unit/UrlUtility.spec.js +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { UrlUtility } from '../../src/UrlUtility'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("UrlUtility", function() { - - describe("addQueryParam", function() { - - it("should add ? if not present", function() { - UrlUtility.addQueryParam("url", "foo", "test").should.equal("url?foo=test"); - }); - - it("should not add ? if already present", function() { - UrlUtility.addQueryParam("url?", "foo", "test").should.equal("url?foo=test"); - }); - - it("should add & if needed", function() { - UrlUtility.addQueryParam("url?x=1", "foo", "test").should.equal("url?x=1&foo=test"); - }); - - it("should urlencode key and value", function() { - UrlUtility.addQueryParam("url", "#", "#").should.equal("url?%23=%23"); - }); - }); - - describe("parseUrlFragment", function() { - - it("should parse key/value pairs", function() { - let result = UrlUtility.parseUrlFragment("a=apple&b=banana&c=carrot"); - result.should.deep.equal({ a: "apple", b: "banana", c: "carrot" }); - }); - - it("should parse any order", function() { - let result = UrlUtility.parseUrlFragment("b=banana&c=carrot&a=apple"); - result.should.deep.equal({ a: "apple", b: "banana", c: "carrot" }); - }); - - it("should parse past host name and hash fragment", function() { - let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=xoxo/#a=apple&b=banana&c=carrot"); - result.should.deep.equal({ a: "apple", b: "banana", c: "carrot" }); - }); - - it("should parse query string", function() { - let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=yoyo", "?"); - result.should.deep.equal({ test1: "xoxo", test2: "yoyo" }); - }); - - it("should parse query string up to hash", function() { - let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=yoyo#a=apple&b=banana&c=carrot", "?"); - result.should.deep.equal({ test1: "xoxo", test2: "yoyo" }); - }); - - it("should return error for long values", function() { - let result = UrlUtility.parseUrlFragment("a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple"); - result.should.have.property('error'); - }); - - it("should use Global.location when no value is passed", function() { - let w = { - location: { - href : "a=apple&b=banana&c=carrot" - } - }; - let result = UrlUtility.parseUrlFragment(null, "#", w); - result.should.deep.equal({ a: "apple", b: "banana", c: "carrot" }); - }); - - it("should return empty object for empty string", function() { - let result = UrlUtility.parseUrlFragment(""); - result.should.deep.equal({}); - }); - }); - -}); diff --git a/test/unit/UrlUtility.test.ts b/test/unit/UrlUtility.test.ts new file mode 100644 index 000000000..f9e51ca95 --- /dev/null +++ b/test/unit/UrlUtility.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { UrlUtility } from '../../src/UrlUtility'; + +describe("UrlUtility", () => { + + describe("addQueryParam", () => { + + it("should add ? if not present", () => { + // act + let result = UrlUtility.addQueryParam("url", "foo", "test"); + + // assert + expect(result).toEqual("url?foo=test"); + }); + + it("should not add ? if already present", () => { + // act + let result = UrlUtility.addQueryParam("url?", "foo", "test"); + + // assert + expect(result).toEqual("url?foo=test"); + }); + + it("should add & if needed", () => { + // act + let result = UrlUtility.addQueryParam("url?x=1", "foo", "test"); + + // assert + expect(result).toEqual("url?x=1&foo=test"); + }); + + it("should urlencode key and value", () => { + // act + let result = UrlUtility.addQueryParam("url", "#", "#"); + + // assert + expect(result).toEqual("url?%23=%23"); + }); + }); + + describe("parseUrlFragment", () => { + + it("should parse key/value pairs", () => { + // act + let result = UrlUtility.parseUrlFragment("a=apple&b=banana&c=carrot"); + + // assert + expect(result).toEqual({ a: "apple", b: "banana", c: "carrot" }); + }); + + it("should parse any order", () => { + // act + let result = UrlUtility.parseUrlFragment("b=banana&c=carrot&a=apple"); + + // assert + expect(result).toEqual({ a: "apple", b: "banana", c: "carrot" }); + }); + + it("should parse past host name and hash fragment", () => { + // act + let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=xoxo/#a=apple&b=banana&c=carrot"); + + // assert + expect(result).toEqual({ a: "apple", b: "banana", c: "carrot" }); + }); + + it("should parse query string", () => { + // act + let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=yoyo", "?"); + + // assert + expect(result).toEqual({ test1: "xoxo", test2: "yoyo" }); + }); + + it("should parse query string up to hash", () => { + // act + let result = UrlUtility.parseUrlFragment("http://server?test1=xoxo&test2=yoyo#a=apple&b=banana&c=carrot", "?"); + + // assert + expect(result).toEqual({ test1: "xoxo", test2: "yoyo" }); + }); + + it("should return error for long values", () => { + // act + let result = UrlUtility.parseUrlFragment("a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple&a=apple"); + + // assert + expect(result).toHaveProperty("error"); + }); + + it("should return empty object for empty string", () => { + // act + let result = UrlUtility.parseUrlFragment(""); + + // assert + expect(result).toEqual({}); + }); + }); +}); diff --git a/test/unit/UserInfoService.spec.js b/test/unit/UserInfoService.spec.js deleted file mode 100644 index 3577e596e..000000000 --- a/test/unit/UserInfoService.spec.js +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { UserInfoService } from '../../src/UserInfoService'; - -import { StubJsonService } from './StubJsonService'; -import { StubMetadataService } from './StubMetadataService'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("UserInfoService", function() { - let subject; - let settings; - let stubJsonService; - let stubMetadataService; - - beforeEach(function() { - settings = {}; - stubJsonService = new StubJsonService(); - stubMetadataService = new StubMetadataService(); - subject = new UserInfoService(settings, () => stubJsonService, () => stubMetadataService); - }); - - describe("constructor", function() { - - it("should require a settings param", function() { - try { - new UserInfoService(); - } - catch (e) { - e.message.should.contain('settings'); - return; - } - assert.fail(); - }); - - }); - - describe("getClaims", function() { - - it("should return a promise", function() { - var p = subject.getClaims(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should require a token", function(done) { - subject.getClaims().catch(err => { - err.message.should.contain("token"); - done(); - }); - }); - - it("should call userinfo endpoint and pass token", function(done) { - stubMetadataService.userInfoEndpointResult = Promise.resolve("http://sts/userinfo"); - stubJsonService.result = Promise.resolve("test"); - - subject.getClaims("token").then(claims => { - stubJsonService.url.should.equal("http://sts/userinfo"); - stubJsonService.token.should.equal("token"); - done(); - }); - - }); - - it("should fail when dependencies fail", function(done) { - stubMetadataService.userInfoEndpointResult = Promise.reject(new Error("test")); - - subject.getClaims("token").then(null, - err => { - err.message.should.contain('test'); - done(); - } - ); - - }); - - it("should return claims", function(done) { - stubMetadataService.userInfoEndpointResult = Promise.resolve("http://sts/userinfo"); - stubJsonService.result = Promise.resolve({ - foo: 1, bar: 'test', - aud:'some_aud', iss:'issuer', - sub:'123', email:'foo@gmail.com', - role:['admin', 'dev'], - nonce:'nonce', at_hash:"athash", - iat:5, nbf:10, exp:20 - }); - - subject.getClaims("token").then(claims => { - claims.should.deep.equal({ - foo: 1, bar: 'test', - aud:'some_aud', iss:'issuer', - sub:'123', email:'foo@gmail.com', - role:['admin', 'dev'], - nonce:'nonce', at_hash:"athash", - iat:5, nbf:10, exp:20 - }); - done(); - }); - - }); - }); -}); diff --git a/test/unit/UserInfoService.test.ts b/test/unit/UserInfoService.test.ts new file mode 100644 index 000000000..34ef7acfb --- /dev/null +++ b/test/unit/UserInfoService.test.ts @@ -0,0 +1,101 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { UserInfoService } from '../../src/UserInfoService'; + +import { StubJsonService } from './StubJsonService'; +import { StubMetadataService } from './StubMetadataService'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("UserInfoService", () => { + let subject: UserInfoService; + let stubJsonService: any; + let stubMetadataService: any; + + beforeEach(() => { + const settings: any = {}; + stubJsonService = new StubJsonService(); + stubMetadataService = new StubMetadataService(); + // @ts-ignore + subject = new UserInfoService(settings, () => stubJsonService, () => stubMetadataService); + }); + + describe("getClaims", () => { + + it("should return a promise", async () => { + // act + var p = subject.getClaims(); + + // assert + expect(p).toBeInstanceOf(Promise); + p.catch(_e => {}); + }); + + it("should require a token", async () => { + // act + try { + await subject.getClaims(); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("token"); + } + }); + + it("should call userinfo endpoint and pass token", async () => { + // arrange + stubMetadataService.userInfoEndpointResult = Promise.resolve("http://sts/userinfo"); + stubJsonService.result = Promise.resolve("test"); + + // act + await subject.getClaims("token"); + + // assert + expect(stubJsonService.url).toEqual("http://sts/userinfo"); + expect(stubJsonService.token).toEqual("token"); + }); + + it("should fail when dependencies fail", async () => { + // arrange + stubMetadataService.userInfoEndpointResult = Promise.reject(new Error("test")); + + // act + try { + await subject.getClaims("token"); + fail("should not come here"); + } catch (err) { + expect(err.message).toContain("test"); + } + }); + + it("should return claims", async () => { + // arrange + stubMetadataService.userInfoEndpointResult = Promise.resolve("http://sts/userinfo"); + const expectedClaims = { + foo: 1, bar: 'test', + aud:'some_aud', iss:'issuer', + sub:'123', email:'foo@gmail.com', + role:['admin', 'dev'], + nonce:'nonce', at_hash:"athash", + iat:5, nbf:10, exp:20 + } + stubJsonService.result = Promise.resolve(expectedClaims); + + // act + const claims = await subject.getClaims("token"); + + // assert + expect(claims).toEqual(expectedClaims); + }); + }); +}); diff --git a/test/unit/UserManager.spec.js b/test/unit/UserManager.spec.js deleted file mode 100644 index dba949f21..000000000 --- a/test/unit/UserManager.spec.js +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { UserManager } from '../../src/UserManager'; -import { Log } from '../../src/Log'; -import { Global } from '../../src/Global'; -import { UserManagerSettings } from '../../src/UserManagerSettings'; -import { User } from '../../src/User'; - -import { StubMetadataService } from './StubMetadataService'; -import { StubSilentRenewService } from './StubSilentRenewService'; -import { StubStateStore } from './StubStateStore'; -import { StubResponseValidator } from './StubResponseValidator'; -import { StubTokenRevocationClient } from './StubTokenRevocationClient'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("UserManager", function () { - let settings; - let subject; - let stubMetadataService; - let stubStateStore; - let stubValidator; - let stubSilentRenewService; - let stubNavigator; - let stubUserStore; - let stubTokenRevocationClient; - - beforeEach(function () { - - Global._testing(); - - Log.logger = console; - Log.level = Log.NONE; - - stubNavigator = {}; - stubUserStore = new StubStateStore(); - stubStateStore = new StubStateStore(); - stubValidator = new StubResponseValidator(); - stubSilentRenewService = new StubSilentRenewService(); - stubMetadataService = new StubMetadataService(); - stubTokenRevocationClient = new StubTokenRevocationClient(); - - settings = { - authority: 'http://sts/oidc', - client_id: 'client', - monitorSession : false, - navigator: stubNavigator, - userStore: stubUserStore, - stateStore: stubStateStore, - ResponseValidatorCtor: () => stubValidator, - MetadataServiceCtor: () => stubMetadataService - }; - - subject = new UserManager(settings, - () => stubSilentRenewService, - null, - () => stubTokenRevocationClient); - }); - - describe("constructor", function () { - - it("should accept settings", function () { - subject.settings.client_id.should.equal('client'); - }); - - }); - - describe("settings", function () { - - it("should be UserManagerSettings", function () { - subject.settings.should.be.instanceof(UserManagerSettings); - }); - - }); - - describe("userLoaded", function () { - - it("should be able to call getUser without recursion", function (done) { - - stubUserStore.item = new User({id_token:"id_token"}).toStorageString(); - - subject.events.addUserLoaded(user => { - subject.getUser().then(user => { - done(); - }); - }); - - subject.events.load({}); - }); - - }); - - describe("signinSilent", function(){ - - it("should pass silentRequestTimeout from settings", function(done) { - - stubUserStore.item = new User({id_token:"id_token"}).toStorageString(); - - settings.silentRequestTimeout = 123; - settings.silent_redirect_uri = "http://client/silent_callback"; - subject = new UserManager(settings); - - subject._signin = function(args, nav, navArgs){ - Log.debug("_signin", args, nav, navArgs); - - navArgs.silentRequestTimeout.should.equal(123); - done(); - return Promise.resolve() - } - subject.signinSilent(); - }); - - it("should pass silentRequestTimeout from params", function(done){ - - stubUserStore.item = new User({id_token:"id_token"}).toStorageString(); - - settings.silent_redirect_uri = "http://client/silent_callback"; - subject = new UserManager(settings); - - subject._signin = function(args, nav, navArgs){ - navArgs.silentRequestTimeout.should.equal(234); - done(); - return Promise.resolve() - } - subject.signinSilent({silentRequestTimeout:234}); - }); - - it("should pass prompt from params", function(done){ - - stubUserStore.item = new User({id_token:"id_token"}).toStorageString(); - - settings.silent_redirect_uri = "http://client/silent_callback"; - subject = new UserManager(settings); - - subject._signin = function(args, nav, navArgs){ - args.prompt.should.equal("foo"); - done(); - return Promise.resolve() - } - subject.signinSilent({prompt:"foo"}); - }); - - it("should work when having no User present", function(done) { - settings.silent_redirect_uri = "http://client/silent_callback"; - subject = new UserManager(settings); - - subject._signin = function(){ - done(); - return Promise.resolve() - } - subject.signinSilent({prompt:"foo"}); - }) - }); - -}); diff --git a/test/unit/UserManager.test.ts b/test/unit/UserManager.test.ts new file mode 100644 index 000000000..8c6116240 --- /dev/null +++ b/test/unit/UserManager.test.ts @@ -0,0 +1,175 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { UserManager } from '../../src/UserManager'; +import { UserManagerSettings, UserManagerSettingsStore } from '../../src/UserManagerSettings'; +import { User } from '../../src/User'; +import { WebStorageStateStore } from '../../src/WebStorageStateStore'; +import { mocked } from 'ts-jest/utils'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("UserManager", () => { + let settings: UserManagerSettings; + let userStoreMock: any; + let subject: UserManager; + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + + userStoreMock = mocked(new WebStorageStateStore()) + + settings = { + authority: "http://sts/oidc", + client_id: "client", + monitorSession : false, + userStore: userStoreMock, + }; + subject = new UserManager(settings); + }); + + describe("constructor", () => { + + it("should accept settings", () => { + // act + expect(subject.settings.client_id).toEqual("client"); + }); + }); + + describe("settings", () => { + + it("should be UserManagerSettings", () => { + // act + expect(subject.settings).toBeInstanceOf(UserManagerSettingsStore); + }); + }); + + describe("userLoaded", () => { + + it("should be able to call getUser without recursion", async () => { + // arrange + const user = new User({ id_token: "id_token" }); + userStoreMock.item = user.toStorageString(); + + subject.events.addUserLoaded(async (_user) => { + await subject.getUser(); + }); + + // act + subject.events.load({} as User); + }); + }); + + describe("signinSilent", () =>{ + + it("should pass silentRequestTimeout from settings", async () => { + // arrange + const user = new User({id_token:"id_token"}); + userStoreMock.item = user.toStorageString(); + + settings = { + ...settings, + silentRequestTimeout: 123, + silent_redirect_uri: "http://client/silent_callback" + }; + subject = new UserManager(settings); + + let navArgs: any = null; + + // @ts-ignore + subject._signin = function(args: any, nav: any, arg_navArgs: any) { + Log.debug("_signin", args, nav, navArgs); + + navArgs = arg_navArgs; + return Promise.resolve() + } + + // act + await subject.signinSilent(); + + // assert + expect(navArgs.silentRequestTimeout).toEqual(123); + }); + + it("should pass silentRequestTimeout from params", async () =>{ + // arrange + const user = new User({id_token:"id_token"}); + userStoreMock.item = user.toStorageString(); + + settings = { + ...settings, + silent_redirect_uri: "http://client/silent_callback" + }; + subject = new UserManager(settings); + + let navArgs: any = null; + + // @ts-ignore + subject._signin = function(args: any, nav: any, arg_navArgs: any) { + navArgs = arg_navArgs; + return Promise.resolve() + } + + // act + await subject.signinSilent({ silentRequestTimeout: 234 }); + + // assert + expect(navArgs.silentRequestTimeout).toEqual(234); + }); + + it("should pass prompt from params", async () =>{ + // arrange + const user = new User({id_token:"id_token"}) + userStoreMock.item = user.toStorageString(); + + settings = { + ...settings, + silent_redirect_uri: "http://client/silent_callback" + }; + subject = new UserManager(settings); + + let args: any = null; + + // @ts-ignore + subject._signin = function(arg_args: any, nav: any, navArgs: any) { + args = arg_args; + return Promise.resolve() + } + + // act + await subject.signinSilent({ prompt:"foo" }); + + // assert + expect(args.prompt).toEqual("foo"); + }); + + it("should work when having no User present", async () => { + // arrange + settings = { + ...settings, + silent_redirect_uri: "http://client/silent_callback" + }; + subject = new UserManager(settings); + + // @ts-ignore + subject._signin = function(args: any, nav: any, arg_navArgs: any) { + return Promise.resolve() + } + + // act + await subject.signinSilent({ prompt:"foo" }); + }); + }); +}); diff --git a/test/unit/UserManagerEvents.spec.js b/test/unit/UserManagerEvents.spec.js deleted file mode 100644 index 72f0e2e0a..000000000 --- a/test/unit/UserManagerEvents.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { UserManagerEvents } from '../../src/UserManagerEvents'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("UserManagerEvents", function () { - - let subject; - - beforeEach(function () { - subject = new UserManagerEvents(); - }); - - describe("silent renew error", function () { - - it("should allow callback", function () { - var cb = function () { - cb.wasCalled = true; - }; - subject.addSilentRenewError(cb); - - subject._raiseSilentRenewError(new Error("boom")); - - cb.wasCalled.should.be.true; - }); - - it("should allow unregistering callback", function () { - var cb = function () { - cb.wasCalled = true; - }; - cb.wasCalled = false; - - subject.addSilentRenewError(cb); - subject.removeSilentRenewError(cb); - - subject._raiseSilentRenewError(new Error("boom")); - - cb.wasCalled.should.be.false; - }); - - it("should pass error to callback", function () { - var cb = function (e) { - e.message.should.equal("boom"); - }; - subject.addSilentRenewError(cb); - - subject._raiseSilentRenewError(new Error("boom")); - }); - - }); - -}); diff --git a/test/unit/UserManagerEvents.test.ts b/test/unit/UserManagerEvents.test.ts new file mode 100644 index 000000000..7c833a754 --- /dev/null +++ b/test/unit/UserManagerEvents.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { UserManagerEvents } from '../../src/UserManagerEvents'; +import { UserManagerSettingsStore } from '../../src/UserManagerSettings'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("UserManagerEvents", () => { + + let subject: UserManagerEvents; + + beforeEach(() => { + let settings = new UserManagerSettingsStore({}); + subject = new UserManagerEvents(settings); + }); + + describe("silent renew error", () => { + + it("should allow callback", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addSilentRenewError(cb); + subject._raiseSilentRenewError(new Error("boom")); + + // assert + expect(cb).toBeCalled(); + }); + + it("should allow unregistering callback", () => { + // arrange + var cb = jest.fn(); + + // act + subject.addSilentRenewError(cb); + subject.removeSilentRenewError(cb); + subject._raiseSilentRenewError(new Error("boom")); + + // assert + expect(cb).toBeCalledTimes(0); + }); + + it("should pass error to callback", () => { + // arrange + let e: Error | null = null; + var cb = function (arg_e: Error) { + e = arg_e; + }; + const expected = new Error("boom"); + + // act + subject.addSilentRenewError(cb); + subject._raiseSilentRenewError(expected); + + // assert + expect(e).toEqual(expected); + }); + }); +}); diff --git a/test/unit/UserManagerSettings.spec.js b/test/unit/UserManagerSettings.spec.js deleted file mode 100644 index 83941a418..000000000 --- a/test/unit/UserManagerSettings.spec.js +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { UserManagerSettings } from '../../src/UserManagerSettings'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; - -describe("UserManagerSettings", function () { - - beforeEach(function () { - Log.logger = console; - Log.level = Log.NONE; - }); - - describe("constructor", function () { - - it("should allow no settings", function () { - let subject = new UserManagerSettings(); - }); - - it("should pass settings to base class", function () { - let subject = new UserManagerSettings({ client_id: 'client' }); - subject.client_id.should.equal('client'); - }); - - }); - - describe("popup_redirect_uri", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ popup_redirect_uri: 'test' }); - subject.popup_redirect_uri.should.equal('test'); - }); - - }); - - describe("popupWindowFeatures", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ popupWindowFeatures: 'foo' }); - subject.popupWindowFeatures.should.equal('foo'); - }); - - }); - - describe("popupWindowTarget", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ popupWindowTarget: 'foo' }); - subject.popupWindowTarget.should.equal('foo'); - }); - - }); - - describe("silent_redirect_uri", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ silent_redirect_uri: 'test' }); - subject.silent_redirect_uri.should.equal('test'); - }); - - }); - - describe("silentRequestTimeout", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ silentRequestTimeout: 123 }); - subject.silentRequestTimeout.should.equal(123); - }); - - }); - - describe("automaticSilentRenew", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ - automaticSilentRenew: true - }); - subject.automaticSilentRenew.should.be.true; - }); - - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.automaticSilentRenew.should.be.false; - }); - - }); - - describe("validateSubOnSilentRenew", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ - validateSubOnSilentRenew: true - }); - subject.validateSubOnSilentRenew.should.be.true; - }); - - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.validateSubOnSilentRenew.should.be.false; - }); - - }); - - describe("includeIdTokenInSilentRenew", function () { - it("should return true value from initial settings", function () { - let subject = new UserManagerSettings({ - includeIdTokenInSilentRenew: true, - }); - subject.includeIdTokenInSilentRenew.should.be.true; - }); - - it("should return false value from initial settings", function () { - let subject = new UserManagerSettings({ - includeIdTokenInSilentRenew: false, - }); - subject.includeIdTokenInSilentRenew.should.be.false; - }); - - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.includeIdTokenInSilentRenew.should.be.true; - }); - }); - - describe("accessTokenExpiringNotificationTime", function () { - - it("should return value from initial settings", function () { - let subject = new UserManagerSettings({ - accessTokenExpiringNotificationTime: 10 - }); - subject.accessTokenExpiringNotificationTime.should.equal(10); - }); - - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.accessTokenExpiringNotificationTime.should.equal(60); - }); - - }); - - describe("redirectNavigator", function() { - it("should return value from initial settings", function() { - let temp = {}; - let subject = new UserManagerSettings({ - redirectNavigator : temp - }); - subject.redirectNavigator.should.equal(temp); - }); - }); - - describe("popupNavigator", function() { - it("should return value from initial settings", function() { - let temp = {}; - let subject = new UserManagerSettings({ - popupNavigator : temp - }); - subject.popupNavigator.should.equal(temp); - }); - }); - - describe("iframeNavigator", function() { - it("should return value from initial settings", function() { - let temp = {}; - let subject = new UserManagerSettings({ - iframeNavigator : temp - }); - subject.iframeNavigator.should.equal(temp); - }); - }); - - describe("redirectNavigator", function() { - it("should return value from initial settings", function() { - let temp = {}; - let subject = new UserManagerSettings({ - userStore : temp - }); - subject.userStore.should.equal(temp); - }); - }); - - describe("revokeAccessTokenOnSignout", function() { - it("should return value from initial settings", function() { - let subject = new UserManagerSettings({ - revokeAccessTokenOnSignout : true - }); - subject.revokeAccessTokenOnSignout.should.equal(true); - }); - }); - - describe("checkSessionInterval", function() { - it("should return value from initial settings", function() { - let subject = new UserManagerSettings({ - checkSessionInterval : 6000 - }); - subject.checkSessionInterval.should.equal(6000); - }); - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.checkSessionInterval.should.equal(2000); - }); - }); - - describe("query_status_response_type", function() { - it("should return value from initial settings", function() { - let temp = 'type'; - let subject = new UserManagerSettings({ - query_status_response_type : temp - }); - subject.query_status_response_type.should.equal(temp); - }); - it("should infer default value", function () { - { - let subject = new UserManagerSettings({ - response_type: "id_token token" - }); - subject.query_status_response_type.should.equal("id_token"); - } - { - let subject = new UserManagerSettings({ - response_type: "code" - }); - subject.query_status_response_type.should.equal("code"); - } - }); - }); - - describe("stopCheckSessionOnError", function() { - it("should return value from initial settings", function() { - let subject = new UserManagerSettings({ - stopCheckSessionOnError : false - }); - subject.stopCheckSessionOnError.should.be.false; - }); - it("should use default value", function () { - let subject = new UserManagerSettings({ - }); - subject.stopCheckSessionOnError.should.be.true; - }); - }); -}); diff --git a/test/unit/UserManagerSettings.test.ts b/test/unit/UserManagerSettings.test.ts new file mode 100644 index 000000000..36aa569a5 --- /dev/null +++ b/test/unit/UserManagerSettings.test.ts @@ -0,0 +1,360 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { Log } from '../../src/Log'; +import { UserManagerSettingsStore } from '../../src/UserManagerSettings'; +import { WebStorageStateStore } from '../../src/WebStorageStateStore'; + +// workaround jest parse error +jest.mock('../../jsrsasign/dist/jsrsasign.js', () => { + return { + jws: jest.fn(), + KEYUTIL: jest.fn(), + X509: jest.fn(), + crypto: jest.fn(), + hextob64u: jest.fn(), + b64tohex: jest.fn() + }; +}); + +describe("UserManagerSettings", () => { + + beforeEach(() => { + Log.logger = console; + Log.level = Log.NONE; + }); + + describe("constructor", () => { + + it("should allow no settings", () => { + // act + let subject = new UserManagerSettingsStore(); + + // assert + expect(subject).not.toBeNull(); + }); + + it("should pass settings to base class", () => { + // act + let subject = new UserManagerSettingsStore({ + client_id: 'client' + }); + + // assert + expect(subject.client_id).toEqual('client'); + }); + + }); + + describe("popup_redirect_uri", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + popup_redirect_uri: 'test' + }); + + // assert + expect(subject.popup_redirect_uri).toEqual('test'); + }); + + }); + + describe("popupWindowFeatures", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + popupWindowFeatures: 'foo' + }); + + // assert + expect(subject.popupWindowFeatures).toEqual('foo'); + }); + + }); + + describe("popupWindowTarget", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + popupWindowTarget: 'foo' + }); + + // assert + expect(subject.popupWindowTarget).toEqual('foo'); + }); + + }); + + describe("silent_redirect_uri", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + silent_redirect_uri: 'test' + }); + + // assert + expect(subject.silent_redirect_uri).toEqual('test'); + }); + + }); + + describe("silentRequestTimeout", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + silentRequestTimeout: 123 + }); + + // assert + expect(subject.silentRequestTimeout).toEqual(123); + }); + + }); + + describe("automaticSilentRenew", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + automaticSilentRenew: true + }); + + // assert + expect(subject.automaticSilentRenew).toEqual(true); + }); + + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.automaticSilentRenew).toEqual(false); + }); + + }); + + describe("validateSubOnSilentRenew", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + validateSubOnSilentRenew: true + }); + + // assert + expect(subject.validateSubOnSilentRenew).toEqual(true); + }); + + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.validateSubOnSilentRenew).toEqual(false); + }); + + }); + + describe("includeIdTokenInSilentRenew", () => { + it("should return true value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + includeIdTokenInSilentRenew: true, + }); + + // assert + expect(subject.includeIdTokenInSilentRenew).toEqual(true); + }); + + it("should return false value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + includeIdTokenInSilentRenew: false, + }); + + // assert + expect(subject.includeIdTokenInSilentRenew).toEqual(false); + }); + + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.includeIdTokenInSilentRenew).toEqual(true); + }); + }); + + describe("accessTokenExpiringNotificationTime", () => { + + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + accessTokenExpiringNotificationTime: 10 + }); + + // assert + expect(subject.accessTokenExpiringNotificationTime).toEqual(10); + }); + + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.accessTokenExpiringNotificationTime).toEqual(60); + }); + + }); + + describe("redirectNavigator", () => { + it("should return value from initial settings", () => { + let temp = {}; + + // act + let subject = new UserManagerSettingsStore({ + redirectNavigator : temp + }); + + // assert + expect(subject.redirectNavigator).toEqual(temp); + }); + }); + + describe("popupNavigator", () => { + it("should return value from initial settings", () => { + let temp = {}; + + // act + let subject = new UserManagerSettingsStore({ + popupNavigator : temp + }); + + // assert + expect(subject.popupNavigator).toEqual(temp); + }); + }); + + describe("iframeNavigator", () => { + it("should return value from initial settings", () => { + let temp = {}; + + // act + let subject = new UserManagerSettingsStore({ + iframeNavigator : temp + }); + + // assert + expect(subject.iframeNavigator).toEqual(temp); + }); + }); + + describe("redirectNavigator", () => { + it("should return value from initial settings", () => { + let temp = {} as WebStorageStateStore; + + // act + let subject = new UserManagerSettingsStore({ + userStore : temp + }); + + // assert + expect(subject.userStore).toEqual(temp); + }); + }); + + describe("revokeAccessTokenOnSignout", () => { + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + revokeAccessTokenOnSignout : true + }); + + // assert + expect(subject.revokeAccessTokenOnSignout).toEqual(true); + }); + }); + + describe("checkSessionInterval", () => { + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + checkSessionInterval : 6000 + }); + + // assert + expect(subject.checkSessionInterval).toEqual(6000); + }); + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.checkSessionInterval).toEqual(2000); + }); + }); + + describe("query_status_response_type", () => { + it("should return value from initial settings", () => { + let temp = 'type'; + + // act + let subject = new UserManagerSettingsStore({ + query_status_response_type : temp + }); + + // assert + expect(subject.query_status_response_type).toEqual(temp); + }); + it("should infer default value", () => { + { + // act + let subject = new UserManagerSettingsStore({ + response_type: "id_token token" + }); + + // assert + expect(subject.query_status_response_type).toEqual("id_token"); + } + { + // act + let subject = new UserManagerSettingsStore({ + response_type: "code" + }); + + // assert + expect(subject.query_status_response_type).toEqual("code"); + } + }); + }); + + describe("stopCheckSessionOnError", () => { + it("should return value from initial settings", () => { + // act + let subject = new UserManagerSettingsStore({ + stopCheckSessionOnError : false + }); + + // assert + expect(subject.stopCheckSessionOnError).toEqual(false); + }); + it("should use default value", () => { + // act + let subject = new UserManagerSettingsStore({ + }); + + // assert + expect(subject.stopCheckSessionOnError).toEqual(true); + }); + }); +}); diff --git a/test/unit/WebStorageStateStore.spec.js b/test/unit/WebStorageStateStore.spec.js deleted file mode 100644 index b5b06bd4d..000000000 --- a/test/unit/WebStorageStateStore.spec.js +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. -// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. - -import { Log } from '../../src/Log'; -import { WebStorageStateStore } from '../../src/WebStorageStateStore'; -import { InMemoryWebStorage } from '../../src/InMemoryWebStorage'; - -import chai from 'chai'; -chai.should(); -let assert = chai.assert; -let expect = chai.expect; - -describe("WebStorageStateStore", function() { - let prefix; - let subject; - let store; - - beforeEach(function() { - prefix = ""; - store = new InMemoryWebStorage(); - subject = new WebStorageStateStore({ prefix: prefix, store: store }); - }); - - describe("set", function() { - - it("should return a promise", function() { - var p = subject.set("key", "value"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should store item", function(done) { - subject.set("key", "value").then(() => { - store.getItem("key").should.equal("value"); - done(); - }) - }); - - it("should use prefix if specified", function(done) { - prefix = "foo."; - subject = new WebStorageStateStore({ prefix: prefix, store: store }); - - subject.set("key", "value").then(() => { - store.getItem(prefix + "key").should.equal("value"); - done(); - }) - }); - - }); - - describe("remove", function() { - - it("should return a promise", function() { - var p = subject.remove("key"); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should remove item", function(done) { - store.setItem("key", "value"); - - subject.remove("key").then(item => { - expect(store.getItem("key")).to.be.undefined; - done(); - }); - }); - - it("should return value if exists", function(done) { - store.setItem("key", "test"); - - subject.remove("key").then(value => { - value.should.equal('test'); - done(); - }); - }); - - it("should return undefined if doesn't exist", function(done) { - subject.remove("key").then(value => { - expect(value).to.be.undefined; - done(); - }); - }); - - it("should use prefix if specified", function(done) { - prefix = "foo."; - subject = new WebStorageStateStore({ prefix: prefix, store: store }); - - store.setItem("foo.key", "value"); - - subject.remove("key").then(item => { - expect(store.getItem("foo.key")).to.be.undefined; - done(); - }); - }); - - }); - - describe("get", function() { - - it("should return a promise", function() { - var p = subject.get("key"); - p.should.be.instanceof(Promise); - }); - - it("should return value if exists", function(done) { - store.setItem("key", "test"); - - subject.get("key").then(value => { - value.should.equal('test'); - done(); - }); - }); - - it("should return undefined if doesn't exist", function(done) { - subject.get("key").then(value => { - expect(value).to.be.undefined; - done(); - }); - }); - - it("should use prefix if specified", function(done) { - prefix = "foo."; - subject = new WebStorageStateStore({ prefix: prefix, store: store }); - - store.setItem("foo.key", "value"); - - subject.get("key").then(item => { - item.should.equal("value"); - done(); - }); - }); - - }); - - describe("getAllKeys", function() { - - it("should return a promise", function() { - var p = subject.getAllKeys(); - p.should.be.instanceof(Promise); - p.catch(e=>{}); - }); - - it("should return keys", function(done) { - store.setItem("key1", "test"); - store.setItem("key2", "test"); - - subject.getAllKeys().then(keys => { - keys.should.deep.equal(["key1", "key2"]); - done(); - }); - }); - - it("should return keys without prefix", function(done) { - prefix = "foo."; - subject = new WebStorageStateStore({ prefix: prefix, store: store }); - - store.setItem("foo.key1", "test"); - store.setItem("foo.key2", "test"); - - subject.getAllKeys().then(keys => { - keys.should.deep.equal(["key1", "key2"]); - done(); - }); - }); - - it("should return empty keys when empty", function(done) { - subject.getAllKeys().then(keys => { - keys.should.deep.equal([]); - done(); - }); - }); - - }); - -}); diff --git a/test/unit/WebStorageStateStore.test.ts b/test/unit/WebStorageStateStore.test.ts new file mode 100644 index 000000000..74fe46be7 --- /dev/null +++ b/test/unit/WebStorageStateStore.test.ts @@ -0,0 +1,196 @@ +// Copyright (c) Brock Allen & Dominick Baier. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +import { WebStorageStateStore } from '../../src/WebStorageStateStore'; +import { InMemoryWebStorage } from '../../src/InMemoryWebStorage'; + +describe("WebStorageStateStore", () => { + let prefix: string; + let store: Storage; + let subject: WebStorageStateStore; + + beforeEach(() => { + prefix = ""; + store = new InMemoryWebStorage(); + subject = new WebStorageStateStore({ prefix: prefix, store: store }); + }); + + describe("set", () => { + + it("should return a promise", async () => { + // act + let p = subject.set("key", "value"); + + // assert + expect(p).toBeInstanceOf(Promise); + await p; + }); + + it("should store item", async () => { + // act + await subject.set("key", "value"); + let result = await store.getItem("key"); + + // assert + expect(result).toEqual("value"); + }); + + it("should use prefix if specified", async () => { + // arrange + prefix = "foo."; + subject = new WebStorageStateStore({ prefix: prefix, store: store }); + + // act + await subject.set("key", "value"); + + // assert + let result = await store.getItem(prefix + "key"); + expect(result).toEqual("value"); + }); + }); + + describe("remove", () => { + + it("should return a promise", () => { + // act + let p = subject.remove("key"); + + // assert + expect(p).toBeInstanceOf(Promise); + }); + + it("should remove item", async () => { + // arrange + await store.setItem("key", "value"); + + // act + await subject.remove("key"); + let result = await store.getItem("key"); + + // assert + expect(result).toBeUndefined(); + }); + + it("should return value if exists", async () => { + // arrange + await store.setItem("key", "test"); + + // act + let result = await subject.remove("key"); + + // assert + expect(result).toEqual("test"); + }); + + it("should return undefined if doesn't exist", async () => { + // act + let result = await subject.remove("key"); + + // assert + expect(result).toBeUndefined(); + }); + + it("should use prefix if specified", async () => { + // arrange + prefix = "foo."; + subject = new WebStorageStateStore({ prefix: prefix, store: store }); + await subject.set("key", "value"); + + // act + let result = await subject.remove("key"); + + // assert + expect(result).toEqual("value"); + }); + + }); + + describe("get", () => { + + it("should return a promise", () => { + // act + var p = subject.get("key"); + + // assert + expect(p).toBeInstanceOf(Promise); + }); + + it("should return value if exists", async () => { + // arrange + await store.setItem("key", "test"); + + // act + let result = await subject.get("key"); + + // assert + expect(result).toEqual("test"); + }); + + it("should return undefined if doesn't exist", async () => { + // act + let result = await subject.get("key"); + + // assert + expect(result).toBeUndefined(); + }); + + it("should use prefix if specified", async () => { + // arrange + prefix = "foo."; + subject = new WebStorageStateStore({ prefix: prefix, store: store }); + store.setItem("foo.key", "value"); + + // act + let result = await subject.get("key"); + + // assert + expect(result).toEqual("value"); + }); + + }); + + describe("getAllKeys", () => { + + it("should return a promise", () => { + // act + var p = subject.getAllKeys(); + + // assert + expect(p).toBeInstanceOf(Promise); + }); + + it("should return keys", async () => { + // arrange + await store.setItem("key1", "test"); + await store.setItem("key2", "test"); + + // act + let result = await subject.getAllKeys(); + + // assert + expect(result).toStrictEqual(["key1", "key2"]); + }); + + it("should return keys without prefix", async () => { + // arrange + prefix = "foo."; + subject = new WebStorageStateStore({ prefix: prefix, store: store }); + await store.setItem("foo.key1", "test"); + await store.setItem("foo.key2", "test"); + + // act + let result = await subject.getAllKeys(); + + // assert + expect(result).toStrictEqual(["key1", "key2"]); + }); + + it("should return empty keys when empty", async () => { + // act + let result = await subject.getAllKeys(); + + // assert + expect(result).toStrictEqual([]); + }); + }); +}); diff --git a/test/unit/random.spec.js b/test/unit/random.test.ts similarity index 50% rename from test/unit/random.spec.js rename to test/unit/random.test.ts index 050cb3010..2b2a77567 100644 --- a/test/unit/random.spec.js +++ b/test/unit/random.test.ts @@ -4,11 +4,12 @@ const pattern = /^[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12 //const pattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ -describe('random', function() { - it('should return a valid RFC4122 v4 guid (sans dashes)', function(){ - const rnd = random() - console.log(rnd); - rnd.should.match(pattern) - }) -}) - +describe('random', () => { + it('should return a valid RFC4122 v4 guid (sans dashes)', () => { + // act + const rnd = random() + + // assert + expect(rnd).toMatch(pattern); + }); +});