From 08a8d36c5ede7c126feabac79dc2926e70f79c2d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 11 Jul 2018 22:01:36 +0200 Subject: [PATCH] improve module unit tests --- scm-ui/flow-typed/npm/jest_v20.x.x.js | 597 ++++++++++++++++++++++++++ scm-ui/package.json | 4 +- scm-ui/src/apiclient.js | 6 +- scm-ui/src/containers/App.js | 12 +- scm-ui/src/modules/login.test.js | 120 ++++-- scm-ui/src/modules/me.test.js | 106 +++++ 6 files changed, 801 insertions(+), 44 deletions(-) create mode 100644 scm-ui/flow-typed/npm/jest_v20.x.x.js create mode 100644 scm-ui/src/modules/me.test.js diff --git a/scm-ui/flow-typed/npm/jest_v20.x.x.js b/scm-ui/flow-typed/npm/jest_v20.x.x.js new file mode 100644 index 0000000000..ef86185d08 --- /dev/null +++ b/scm-ui/flow-typed/npm/jest_v20.x.x.js @@ -0,0 +1,597 @@ +// flow-typed signature: 002f0912eb0f40f562c348561ea3d850 +// flow-typed version: a5bbe16c29/jest_v20.x.x/flow_>=v0.39.x + +type JestMockFn, TReturn> = { + (...args: TArguments): TReturn, + /** + * An object for introspecting mock calls + */ + mock: { + /** + * An array that represents all calls that have been made into this mock + * function. Each call is represented by an array of arguments that were + * passed during the call. + */ + calls: Array, + /** + * An array that contains all the object instances that have been + * instantiated from this mock function. + */ + instances: Array + }, + /** + * Resets all information stored in the mockFn.mock.calls and + * mockFn.mock.instances arrays. Often this is useful when you want to clean + * up a mock's usage data between two assertions. + */ + mockClear(): void, + /** + * Resets all information stored in the mock. This is useful when you want to + * completely restore a mock back to its initial state. + */ + mockReset(): void, + /** + * Removes the mock and restores the initial implementation. This is useful + * when you want to mock functions in certain test cases and restore the + * original implementation in others. Beware that mockFn.mockRestore only + * works when mock was created with jest.spyOn. Thus you have to take care of + * restoration yourself when manually assigning jest.fn(). + */ + mockRestore(): void, + /** + * Accepts a function that should be used as the implementation of the mock. + * The mock itself will still record all calls that go into and instances + * that come from itself -- the only difference is that the implementation + * will also be executed when the mock is called. + */ + mockImplementation( + fn: (...args: TArguments) => TReturn, + ): JestMockFn, + /** + * Accepts a function that will be used as an implementation of the mock for + * one call to the mocked function. Can be chained so that multiple function + * calls produce different results. + */ + mockImplementationOnce( + fn: (...args: TArguments) => TReturn, + ): JestMockFn, + /** + * Just a simple sugar function for returning `this` + */ + mockReturnThis(): void, + /** + * Deprecated: use jest.fn(() => value) instead + */ + mockReturnValue(value: TReturn): JestMockFn, + /** + * Sugar for only returning a value once inside your mock + */ + mockReturnValueOnce(value: TReturn): JestMockFn +}; + +type JestAsymmetricEqualityType = { + /** + * A custom Jasmine equality tester + */ + asymmetricMatch(value: mixed): boolean +}; + +type JestCallsType = { + allArgs(): mixed, + all(): mixed, + any(): boolean, + count(): number, + first(): mixed, + mostRecent(): mixed, + reset(): void +}; + +type JestClockType = { + install(): void, + mockDate(date: Date): void, + tick(milliseconds?: number): void, + uninstall(): void +}; + +type JestMatcherResult = { + message?: string | (() => string), + pass: boolean +}; + +type JestMatcher = (actual: any, expected: any) => JestMatcherResult; + +type JestPromiseType = { + /** + * Use rejects to unwrap the reason of a rejected promise so any other + * matcher can be chained. If the promise is fulfilled the assertion fails. + */ + rejects: JestExpectType, + /** + * Use resolves to unwrap the value of a fulfilled promise so any other + * matcher can be chained. If the promise is rejected the assertion fails. + */ + resolves: JestExpectType +}; + +/** + * Plugin: jest-enzyme + */ +type EnzymeMatchersType = { + toBeChecked(): void, + toBeDisabled(): void, + toBeEmpty(): void, + toBeEmptyRender(): void, + toBePresent(): void, + toContainReact(element: React$Element): void, + toExist(): void, + toHaveClassName(className: string): void, + toHaveHTML(html: string): void, + toHaveProp: ((propKey: string, propValue?: any) => void) & ((props: Object) => void), + toHaveRef(refName: string): void, + toHaveState: ((stateKey: string, stateValue?: any) => void) & ((state: Object) => void), + toHaveStyle: ((styleKey: string, styleValue?: any) => void) & ((style: Object) => void), + toHaveTagName(tagName: string): void, + toHaveText(text: string): void, + toIncludeText(text: string): void, + toHaveValue(value: any): void, + toMatchElement(element: React$Element): void, + toMatchSelector(selector: string): void +}; + +// DOM testing library extensions https://github.com/kentcdodds/dom-testing-library#custom-jest-matchers +type DomTestingLibraryType = { + toBeInTheDOM(): void, + toHaveTextContent(content: string): void, + toHaveAttribute(name: string, expectedValue?: string): void +}; + +type JestExpectType = { + not: JestExpectType & EnzymeMatchersType & DomTestingLibraryType, + /** + * If you have a mock function, you can use .lastCalledWith to test what + * arguments it was last called with. + */ + lastCalledWith(...args: Array): void, + /** + * toBe just checks that a value is what you expect. It uses === to check + * strict equality. + */ + toBe(value: any): void, + /** + * Use .toHaveBeenCalled to ensure that a mock function got called. + */ + toBeCalled(): void, + /** + * Use .toBeCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toBeCalledWith(...args: Array): void, + /** + * Using exact equality with floating point numbers is a bad idea. Rounding + * means that intuitive things fail. + */ + toBeCloseTo(num: number, delta: any): void, + /** + * Use .toBeDefined to check that a variable is not undefined. + */ + toBeDefined(): void, + /** + * Use .toBeFalsy when you don't care what a value is, you just want to + * ensure a value is false in a boolean context. + */ + toBeFalsy(): void, + /** + * To compare floating point numbers, you can use toBeGreaterThan. + */ + toBeGreaterThan(number: number): void, + /** + * To compare floating point numbers, you can use toBeGreaterThanOrEqual. + */ + toBeGreaterThanOrEqual(number: number): void, + /** + * To compare floating point numbers, you can use toBeLessThan. + */ + toBeLessThan(number: number): void, + /** + * To compare floating point numbers, you can use toBeLessThanOrEqual. + */ + toBeLessThanOrEqual(number: number): void, + /** + * Use .toBeInstanceOf(Class) to check that an object is an instance of a + * class. + */ + toBeInstanceOf(cls: Class<*>): void, + /** + * .toBeNull() is the same as .toBe(null) but the error messages are a bit + * nicer. + */ + toBeNull(): void, + /** + * Use .toBeTruthy when you don't care what a value is, you just want to + * ensure a value is true in a boolean context. + */ + toBeTruthy(): void, + /** + * Use .toBeUndefined to check that a variable is undefined. + */ + toBeUndefined(): void, + /** + * Use .toContain when you want to check that an item is in a list. For + * testing the items in the list, this uses ===, a strict equality check. + */ + toContain(item: any): void, + /** + * Use .toContainEqual when you want to check that an item is in a list. For + * testing the items in the list, this matcher recursively checks the + * equality of all fields, rather than checking for object identity. + */ + toContainEqual(item: any): void, + /** + * Use .toEqual when you want to check that two objects have the same value. + * This matcher recursively checks the equality of all fields, rather than + * checking for object identity. + */ + toEqual(value: any): void, + /** + * Use .toHaveBeenCalled to ensure that a mock function got called. + */ + toHaveBeenCalled(): void, + /** + * Use .toHaveBeenCalledTimes to ensure that a mock function got called exact + * number of times. + */ + toHaveBeenCalledTimes(number: number): void, + /** + * Use .toHaveBeenCalledWith to ensure that a mock function was called with + * specific arguments. + */ + toHaveBeenCalledWith(...args: Array): void, + /** + * Use .toHaveBeenLastCalledWith to ensure that a mock function was last called + * with specific arguments. + */ + toHaveBeenLastCalledWith(...args: Array): void, + /** + * Check that an object has a .length property and it is set to a certain + * numeric value. + */ + toHaveLength(number: number): void, + /** + * + */ + toHaveProperty(propPath: string, value?: any): void, + /** + * Use .toMatch to check that a string matches a regular expression or string. + */ + toMatch(regexpOrString: RegExp | string): void, + /** + * Use .toMatchObject to check that a javascript object matches a subset of the properties of an object. + */ + toMatchObject(object: Object): void, + /** + * This ensures that a React component matches the most recent snapshot. + */ + toMatchSnapshot(name?: string): void, + /** + * Use .toThrow to test that a function throws when it is called. + * If you want to test that a specific error gets thrown, you can provide an + * argument to toThrow. The argument can be a string for the error message, + * a class for the error, or a regex that should match the error. + * + * Alias: .toThrowError + */ + toThrow(message?: string | Error | RegExp): void, + toThrowError(message?: string | Error | RegExp): void, + /** + * Use .toThrowErrorMatchingSnapshot to test that a function throws a error + * matching the most recent snapshot when it is called. + */ + toThrowErrorMatchingSnapshot(): void +}; + +type JestObjectType = { + /** + * Disables automatic mocking in the module loader. + * + * After this method is called, all `require()`s will return the real + * versions of each module (rather than a mocked version). + */ + disableAutomock(): JestObjectType, + /** + * An un-hoisted version of disableAutomock + */ + autoMockOff(): JestObjectType, + /** + * Enables automatic mocking in the module loader. + */ + enableAutomock(): JestObjectType, + /** + * An un-hoisted version of enableAutomock + */ + autoMockOn(): JestObjectType, + /** + * Clears the mock.calls and mock.instances properties of all mocks. + * Equivalent to calling .mockClear() on every mocked function. + */ + clearAllMocks(): JestObjectType, + /** + * Resets the state of all mocks. Equivalent to calling .mockReset() on every + * mocked function. + */ + resetAllMocks(): JestObjectType, + /** + * Removes any pending timers from the timer system. + */ + clearAllTimers(): void, + /** + * The same as `mock` but not moved to the top of the expectation by + * babel-jest. + */ + doMock(moduleName: string, moduleFactory?: any): JestObjectType, + /** + * The same as `unmock` but not moved to the top of the expectation by + * babel-jest. + */ + dontMock(moduleName: string): JestObjectType, + /** + * Returns a new, unused mock function. Optionally takes a mock + * implementation. + */ + fn, TReturn>( + implementation?: (...args: TArguments) => TReturn, + ): JestMockFn, + /** + * Determines if the given function is a mocked function. + */ + isMockFunction(fn: Function): boolean, + /** + * Given the name of a module, use the automatic mocking system to generate a + * mocked version of the module for you. + */ + genMockFromModule(moduleName: string): any, + /** + * Mocks a module with an auto-mocked version when it is being required. + * + * The second argument can be used to specify an explicit module factory that + * is being run instead of using Jest's automocking feature. + * + * The third argument can be used to create virtual mocks -- mocks of modules + * that don't exist anywhere in the system. + */ + mock( + moduleName: string, + moduleFactory?: any, + options?: Object + ): JestObjectType, + /** + * Resets the module registry - the cache of all required modules. This is + * useful to isolate modules where local state might conflict between tests. + */ + resetModules(): JestObjectType, + /** + * Exhausts the micro-task queue (usually interfaced in node via + * process.nextTick). + */ + runAllTicks(): void, + /** + * Exhausts the macro-task queue (i.e., all tasks queued by setTimeout(), + * setInterval(), and setImmediate()). + */ + runAllTimers(): void, + /** + * Exhausts all tasks queued by setImmediate(). + */ + runAllImmediates(): void, + /** + * Executes only the macro task queue (i.e. all tasks queued by setTimeout() + * or setInterval() and setImmediate()). + */ + runTimersToTime(msToRun: number): void, + /** + * Executes only the macro-tasks that are currently pending (i.e., only the + * tasks that have been queued by setTimeout() or setInterval() up to this + * point) + */ + runOnlyPendingTimers(): void, + /** + * Explicitly supplies the mock object that the module system should return + * for the specified module. Note: It is recommended to use jest.mock() + * instead. + */ + setMock(moduleName: string, moduleExports: any): JestObjectType, + /** + * Indicates that the module system should never return a mocked version of + * the specified module from require() (e.g. that it should always return the + * real module). + */ + unmock(moduleName: string): JestObjectType, + /** + * Instructs Jest to use fake versions of the standard timer functions + * (setTimeout, setInterval, clearTimeout, clearInterval, nextTick, + * setImmediate and clearImmediate). + */ + useFakeTimers(): JestObjectType, + /** + * Instructs Jest to use the real versions of the standard timer functions. + */ + useRealTimers(): JestObjectType, + /** + * Creates a mock function similar to jest.fn but also tracks calls to + * object[methodName]. + */ + spyOn(object: Object, methodName: string): JestMockFn +}; + +type JestSpyType = { + calls: JestCallsType +}; + +/** Runs this function after every test inside this context */ +declare function afterEach(fn: (done: () => void) => ?Promise, timeout?: number): void; +/** Runs this function before every test inside this context */ +declare function beforeEach(fn: (done: () => void) => ?Promise, timeout?: number): void; +/** Runs this function after all tests have finished inside this context */ +declare function afterAll(fn: (done: () => void) => ?Promise, timeout?: number): void; +/** Runs this function before any tests have started inside this context */ +declare function beforeAll(fn: (done: () => void) => ?Promise, timeout?: number): void; + +/** A context for grouping tests together */ +declare var describe: { + /** + * Creates a block that groups together several related tests in one "test suite" + */ + (name: string, fn: () => void): void, + + /** + * Only run this describe block + */ + only(name: string, fn: () => void): void, + + /** + * Skip running this describe block + */ + skip(name: string, fn: () => void): void, +}; + + +/** An individual test unit */ +declare var it: { + /** + * An individual test unit + * + * @param {string} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + (name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, + /** + * Only run this test + * + * @param {string} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + only(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, + /** + * Skip running this test + * + * @param {string} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + skip(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, + /** + * Run the test concurrently + * + * @param {string} Name of Test + * @param {Function} Test + * @param {number} Timeout for the test, in milliseconds. + */ + concurrent(name: string, fn?: (done: () => void) => ?Promise, timeout?: number): void, +}; +declare function fit( + name: string, + fn: (done: () => void) => ?Promise, + timeout?: number, +): void; +/** An individual test unit */ +declare var test: typeof it; +/** A disabled group of tests */ +declare var xdescribe: typeof describe; +/** A focused group of tests */ +declare var fdescribe: typeof describe; +/** A disabled individual test */ +declare var xit: typeof it; +/** A disabled individual test */ +declare var xtest: typeof it; + +type JestPrettyFormatColors = { + comment: { close: string, open: string }, + content: { close: string, open: string }, + prop: { close: string, open: string }, + tag: { close: string, open: string }, + value: { close: string, open: string }, +}; + +type JestPrettyFormatIndent = string => string; +type JestPrettyFormatRefs = Array; +type JestPrettyFormatPrint = any => string; +type JestPrettyFormatStringOrNull = string | null; + +type JestPrettyFormatOptions = {| + callToJSON: boolean, + edgeSpacing: string, + escapeRegex: boolean, + highlight: boolean, + indent: number, + maxDepth: number, + min: boolean, + plugins: JestPrettyFormatPlugins, + printFunctionName: boolean, + spacing: string, + theme: {| + comment: string, + content: string, + prop: string, + tag: string, + value: string, + |}, +|}; + +type JestPrettyFormatPlugin = { + print: ( + val: any, + serialize: JestPrettyFormatPrint, + indent: JestPrettyFormatIndent, + opts: JestPrettyFormatOptions, + colors: JestPrettyFormatColors, + ) => string, + test: any => boolean, +}; + +type JestPrettyFormatPlugins = Array; + +/** The expect function is used every time you want to test a value */ +declare var expect: { + /** The object that you want to make assertions against */ + (value: any): JestExpectType & JestPromiseType & EnzymeMatchersType & DomTestingLibraryType, + /** Add additional Jasmine matchers to Jest's roster */ + extend(matchers: { [name: string]: JestMatcher }): void, + /** Add a module that formats application-specific data structures. */ + addSnapshotSerializer(pluginModule: JestPrettyFormatPlugin): void, + assertions(expectedAssertions: number): void, + hasAssertions(): void, + any(value: mixed): JestAsymmetricEqualityType, + anything(): void, + arrayContaining(value: Array): void, + objectContaining(value: Object): void, + /** Matches any received string that contains the exact expected string. */ + stringContaining(value: string): void, + stringMatching(value: string | RegExp): void +}; + +// TODO handle return type +// http://jasmine.github.io/2.4/introduction.html#section-Spies +declare function spyOn(value: mixed, method: string): Object; + +/** Holds all functions related to manipulating test runner */ +declare var jest: JestObjectType; + +/** + * The global Jasmine object, this is generally not exposed as the public API, + * using features inside here could break in later versions of Jest. + */ +declare var jasmine: { + DEFAULT_TIMEOUT_INTERVAL: number, + any(value: mixed): JestAsymmetricEqualityType, + anything(): void, + arrayContaining(value: Array): void, + clock(): JestClockType, + createSpy(name: string): JestSpyType, + createSpyObj( + baseName: string, + methodNames: Array + ): { [methodName: string]: JestSpyType }, + objectContaining(value: Object): void, + stringMatching(value: string): void +}; diff --git a/scm-ui/package.json b/scm-ui/package.json index c482349665..c93ac29ca6 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -38,12 +38,14 @@ "devDependencies": { "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", + "fetch-mock": "^6.5.0", "flow-bin": "^0.75.0", "flow-typed": "^2.5.1", "node-sass-chokidar": "^1.3.0", "npm-run-all": "^4.1.3", "prettier": "^1.13.7", - "react-test-renderer": "^16.4.1" + "react-test-renderer": "^16.4.1", + "redux-mock-store": "^1.5.3" }, "babel": { "presets": [ diff --git a/scm-ui/src/apiclient.js b/scm-ui/src/apiclient.js index 627bfacd27..f8fd7f8f3f 100644 --- a/scm-ui/src/apiclient.js +++ b/scm-ui/src/apiclient.js @@ -30,7 +30,11 @@ function createUrl(url: string) { if (url.indexOf("://") > 0) { return url; } - return `${apiUrl}/api/rest/v2/${url}`; + let urlWithEndingSlash = url; + if (url.indexOf("/") !== 0) { + urlWithEndingSlash += "/"; + } + return `${apiUrl}/api/rest/v2${urlWithEndingSlash}`; } class ApiClient { diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index c8da53a852..dd54ab8001 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -12,12 +12,13 @@ import PrimaryNavigation from "../components/PrimaryNavigation"; import Loading from "../components/Loading"; import Notification from "../components/Notification"; import Footer from "../components/Footer"; +import ErrorNotification from "../components/ErrorNotification"; type Props = { - login: boolean, me: any, - fetchMe: () => void, - loading: boolean + error: Error, + loading: boolean, + fetchMe: () => void }; class App extends Component { @@ -25,13 +26,16 @@ class App extends Component { this.props.fetchMe(); } render() { - const { me, loading } = this.props; + const { me, loading, error } = this.props; let content = []; let navigation; if (loading) { content.push(); + } else if (error) { + // TODO add error page instead of plain notification + content.push(); } else if (!me) { content.push(); } else { diff --git a/scm-ui/src/modules/login.test.js b/scm-ui/src/modules/login.test.js index dd009a9ee8..b6968ac267 100644 --- a/scm-ui/src/modules/login.test.js +++ b/scm-ui/src/modules/login.test.js @@ -1,49 +1,93 @@ // @flow import reducer, { + login, LOGIN_REQUEST, LOGIN_FAILED, - IS_AUTHENTICATED, - IS_NOT_AUTHENTICATED + LOGIN_SUCCESSFUL } from "./login"; -import { LOGIN, LOGIN_SUCCESSFUL } from "./login"; -test("login", () => { - var newState = reducer({}, { type: LOGIN }); - expect(newState.login).toBe(false); - expect(newState.error).toBe(null); +import { ME_AUTHENTICATED_REQUEST, ME_AUTHENTICATED_SUCCESS } from "./me"; + +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +describe("action tests", () => { + const mockStore = configureMockStore([thunk]); + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + test("login success", () => { + fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", { + body: { + cookie: true, + grant_type: "password", + username: "tricia", + password: "secret123" + }, + headers: { "content-type": "application/json" } + }); + + fetchMock.getOnce("/scm/api/rest/v2/me", { + body: { + username: "tricia" + }, + headers: { "content-type": "application/json" } + }); + + const expectedActions = [ + { type: LOGIN_REQUEST }, + { type: ME_AUTHENTICATED_REQUEST }, + { type: LOGIN_SUCCESSFUL } + ]; + + const store = mockStore({}); + + return store.dispatch(login("tricia", "secret123")).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + test("login failed", () => { + fetchMock.postOnce("/scm/api/rest/v2/auth/access_token", { + status: 400 + }); + + const expectedActions = [{ type: LOGIN_REQUEST }, { type: LOGIN_FAILED }]; + + const store = mockStore({}); + return store.dispatch(login("tricia", "secret123")).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(LOGIN_REQUEST); + expect(actions[1].type).toEqual(LOGIN_FAILED); + expect(actions[1].payload).toBeDefined(); + }); + }); }); -test("login request", () => { - var newState = reducer({}, { type: LOGIN_REQUEST }); - expect(newState.login).toBe(undefined); -}); +describe("reducer tests", () => { + test("login request", () => { + var newState = reducer({}, { type: LOGIN_REQUEST }); + expect(newState.loading).toBeTruthy(); + expect(newState.login).toBeFalsy(); + expect(newState.error).toBeNull(); + }); -test("login successful", () => { - var newState = reducer({ login: false }, { type: LOGIN_SUCCESSFUL }); - expect(newState.login).toBe(true); - expect(newState.error).toBe(null); -}); + test("login successful", () => { + var newState = reducer({ login: false }, { type: LOGIN_SUCCESSFUL }); + expect(newState.loading).toBeFalsy(); + expect(newState.login).toBeTruthy(); + expect(newState.error).toBe(null); + }); -test("login failed", () => { - var newState = reducer({}, { type: LOGIN_FAILED, payload: "error!" }); - expect(newState.login).toBe(false); - expect(newState.error).toBe("error!"); -}); - -test("is authenticated", () => { - var newState = reducer( - { login: false }, - { type: IS_AUTHENTICATED, username: "test" } - ); - expect(newState.login).toBeTruthy(); - expect(newState.username).toBe("test"); -}); - -test("is not authenticated", () => { - var newState = reducer( - { login: true, username: "foo" }, - { type: IS_NOT_AUTHENTICATED } - ); - expect(newState.login).toBe(false); - expect(newState.username).toBeNull(); + test("login failed", () => { + const err = new Error("error!"); + var newState = reducer({}, { type: LOGIN_FAILED, payload: err }); + expect(newState.loading).toBeFalsy(); + expect(newState.login).toBeFalsy(); + expect(newState.error).toBe(err); + }); }); diff --git a/scm-ui/src/modules/me.test.js b/scm-ui/src/modules/me.test.js new file mode 100644 index 0000000000..2d667765fa --- /dev/null +++ b/scm-ui/src/modules/me.test.js @@ -0,0 +1,106 @@ +// @flow +import reducer, { + ME_AUTHENTICATED_REQUEST, + ME_AUTHENTICATED_SUCCESS, + ME_AUTHENTICATED_FAILURE, + ME_UNAUTHENTICATED, + fetchMe +} from "./me"; + +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +describe("fetch tests", () => { + const mockStore = configureMockStore([thunk]); + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + test("successful me fetch", () => { + fetchMock.getOnce("/scm/api/rest/v2/me", { + body: { username: "sorbot" }, + headers: { "content-type": "application/json" } + }); + + const expectedActions = [ + { type: ME_AUTHENTICATED_REQUEST }, + { type: ME_AUTHENTICATED_SUCCESS, payload: { username: "sorbot" } } + ]; + + const store = mockStore({}); + + return store.dispatch(fetchMe()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + test("me fetch failed", () => { + fetchMock.getOnce("/scm/api/rest/v2/me", { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchMe()).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(ME_AUTHENTICATED_REQUEST); + expect(actions[1].type).toEqual(ME_AUTHENTICATED_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); + + test("me fetch unauthenticated", () => { + fetchMock.getOnce("/scm/api/rest/v2/me", { + status: 401 + }); + + const expectedActions = [ + { type: ME_AUTHENTICATED_REQUEST }, + { type: ME_UNAUTHENTICATED } + ]; + + const store = mockStore({}); + + return store.dispatch(fetchMe()).then(() => { + // return of async actions + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); + +describe("reducer tests", () => { + test("me request", () => { + var newState = reducer({}, { type: ME_AUTHENTICATED_REQUEST }); + expect(newState.loading).toBeTruthy(); + expect(newState.me).toBeNull(); + expect(newState.error).toBeNull(); + }); + + test("fetch me successful", () => { + const me = { username: "tricia" }; + var newState = reducer({}, { type: ME_AUTHENTICATED_SUCCESS, payload: me }); + expect(newState.loading).toBeFalsy(); + expect(newState.me).toBe(me); + expect(newState.error).toBe(null); + }); + + test("fetch me failed", () => { + const err = new Error("error!"); + var newState = reducer( + {}, + { type: ME_AUTHENTICATED_FAILURE, payload: err } + ); + expect(newState.loading).toBeFalsy(); + expect(newState.me).toBeNull(); + expect(newState.error).toBe(err); + }); + + test("me unauthenticated", () => { + var newState = reducer({}, { type: ME_UNAUTHENTICATED }); + expect(newState.loading).toBeFalsy(); + expect(newState.me).toBeNull(); + expect(newState.error).toBeNull(); + }); +});