Merge branch '21666-provision-test-improvement'
[arvados.git] / services / workbench2 / src / store / auth / auth-action.test.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { getNewExtraToken, initAuth } from "./auth-action";
6 import { API_TOKEN_KEY } from "services/auth-service/auth-service";
7
8 import 'jest-localstorage-mock';
9 import { ServiceRepository, createServices } from "services/services";
10 import { configureStore, RootStore } from "../store";
11 import { createBrowserHistory } from "history";
12 import { mockConfig } from 'common/config';
13 import { ApiActions } from "services/api/api-actions";
14 import { ACCOUNT_LINK_STATUS_KEY } from 'services/link-account-service/link-account-service';
15 import Axios, { AxiosInstance } from "axios";
16 import MockAdapter from "axios-mock-adapter";
17 import { ImportMock } from 'ts-mock-imports';
18 import * as servicesModule from "services/services";
19 import * as authActionSessionModule from "./auth-action-session";
20 import { SessionStatus } from "models/session";
21 import { getRemoteHostConfig } from "./auth-action-session";
22
23 describe('auth-actions', () => {
24     let axiosInst: AxiosInstance;
25     let axiosMock: MockAdapter;
26
27     let store: RootStore;
28     let services: ServiceRepository;
29     const config: any = {};
30     const actions: ApiActions = {
31         progressFn: (id: string, working: boolean) => { },
32         errorFn: (id: string, message: string) => { }
33     };
34     let importMocks: any[];
35
36     beforeEach(() => {
37         axiosInst = Axios.create({ headers: {} });
38         axiosMock = new MockAdapter(axiosInst);
39         services = createServices(mockConfig({}), actions, axiosInst);
40         store = configureStore(createBrowserHistory(), services, config);
41         localStorage.clear();
42         importMocks = [];
43     });
44
45     afterEach(() => {
46         importMocks.map(m => m.restore());
47     });
48
49     it('creates an extra token', async () => {
50         axiosMock
51             .onGet("/users/current")
52             .reply(200, {
53                 email: "test@test.com",
54                 first_name: "John",
55                 last_name: "Doe",
56                 uuid: "zzzzz-tpzed-abcefg",
57                 owner_uuid: "ownerUuid",
58                 is_admin: false,
59                 is_active: true,
60                 username: "jdoe",
61                 prefs: {}
62             })
63             .onGet("/api_client_authorizations/current")
64             .reply(200, {
65                 expires_at: "2140-01-01T00:00:00.000Z",
66                 api_token: 'extra token',
67             })
68             .onPost("/api_client_authorizations")
69             .replyOnce(200, {
70                 uuid: 'zzzzz-gj3su-xxxxxxxxxx',
71                 apiToken: 'extra token',
72             })
73             .onPost("/api_client_authorizations")
74             .reply(200, {
75                 uuid: 'zzzzz-gj3su-xxxxxxxxxx',
76                 apiToken: 'extra additional token',
77             });
78
79         importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
80         sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
81         localStorage.setItem(API_TOKEN_KEY, "token");
82
83         const config: any = {
84             rootUrl: "https://zzzzz.example.com",
85             uuidPrefix: "zzzzz",
86             remoteHosts: {},
87             apiRevision: 12345678,
88             clusterConfig: {
89                 Login: { LoginCluster: "" },
90                 Workbench: { UserProfileFormFields: {} }
91             },
92         };
93
94         // Set up auth, confirm that no extra token was requested
95         await store.dispatch(initAuth(config))
96         expect(store.getState().auth.apiToken).toBeDefined();
97         expect(store.getState().auth.extraApiToken).toBeUndefined();
98
99         // Ask for an extra token
100         await store.dispatch(getNewExtraToken());
101         expect(store.getState().auth.apiToken).toBeDefined();
102         expect(store.getState().auth.extraApiToken).toBeDefined();
103         const extraToken = store.getState().auth.extraApiToken;
104
105         // Ask for a cached extra token
106         await store.dispatch(getNewExtraToken(true));
107         expect(store.getState().auth.extraApiToken).toBe(extraToken);
108
109         // Ask for a new extra token, should make a second api request
110         await store.dispatch(getNewExtraToken(false));
111         expect(store.getState().auth.extraApiToken).toBeDefined();
112         expect(store.getState().auth.extraApiToken).not.toBe(extraToken);
113     });
114
115     it('requests remote token data to login cluster', async () => {
116         const localClusterTokenExpiration = "2020-01-01T00:00:00.000Z";
117         const loginClusterTokenExpiration = "2140-01-01T00:00:00.000Z";
118         axiosMock
119             .onGet("/users/current")
120             .reply(200, {
121                 email: "test@test.com",
122                 first_name: "John",
123                 last_name: "Doe",
124                 uuid: "zzzz1-tpzed-abcefg",
125                 owner_uuid: "ownerUuid",
126                 is_admin: false,
127                 is_active: true,
128                 username: "jdoe",
129                 prefs: {}
130             })
131             .onGet("https://zzzz1.example.com/discovery/v1/apis/arvados/v1/rest")
132             .reply(200, {
133                 baseUrl: "https://zzzz1.example.com/arvados/v1",
134                 keepWebServiceUrl: "",
135                 keepWebInlineServiceUrl: "",
136                 remoteHosts: {},
137                 rootUrl: "https://zzzz1.example.com",
138                 uuidPrefix: "zzzz1",
139                 websocketUrl: "",
140                 workbenchUrl: "",
141                 workbench2Url: "",
142                 revision: 12345678
143             })
144             // Local cluster -- cached token
145             .onGet("https://zzzzz.example.com/arvados/v1/api_client_authorizations/current")
146             .reply(200, {
147                 uuid: 'zzzz1-gj3su-aaaaaaa',
148                 expires_at: localClusterTokenExpiration,
149                 api_token: 'tokensecret',
150             })
151             // Login cluster -- authoritative token copy
152             .onGet("https://zzzz1.example.com/arvados/v1/api_client_authorizations/current")
153             .reply(200, {
154                 uuid: 'zzzz1-gj3su-aaaaaaa',
155                 expires_at: loginClusterTokenExpiration,
156                 api_token: 'tokensecret',
157             });
158
159         const config: any = {
160             rootUrl: "https://zzzzz.example.com",
161             uuidPrefix: "zzzzz",
162             remoteHosts: { zzzz1: "zzzz1.example.com" },
163             apiRevision: 12345678,
164             clusterConfig: {
165                 Login: { LoginCluster: "zzzz1" },
166                 Workbench: { UserProfileFormFields: {} }
167             },
168         };
169
170         const remoteHostConfig = await getRemoteHostConfig(config.remoteHosts.zzzz1, axiosInst);
171         expect(remoteHostConfig).not.toBeFalsy;
172         services = createServices(remoteHostConfig!, actions, axiosInst);
173
174         importMocks.push(ImportMock.mockFunction(authActionSessionModule, 'getRemoteHostConfig', remoteHostConfig));
175         importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
176
177         sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
178         localStorage.setItem(API_TOKEN_KEY, "v2/zzzz1-gj3su-aaaaaaa/tokensecret");
179
180         await store.dispatch(initAuth(config));
181         expect(store.getState().auth.apiToken).toBeDefined();
182         expect(localClusterTokenExpiration).not.toBe(loginClusterTokenExpiration);
183         expect(store.getState().auth.apiTokenExpiration).toEqual(new Date(loginClusterTokenExpiration));
184     });
185
186     it('should initialise state with user and api token from local storage', (done) => {
187         axiosMock
188             .onGet("/users/current")
189             .reply(200, {
190                 email: "test@test.com",
191                 first_name: "John",
192                 last_name: "Doe",
193                 uuid: "zzzzz-tpzed-abcefg",
194                 owner_uuid: "ownerUuid",
195                 is_admin: false,
196                 is_active: true,
197                 username: "jdoe",
198                 prefs: {}
199             })
200             .onGet("/api_client_authorizations/current")
201             .reply(200, {
202                 expires_at: "2140-01-01T00:00:00.000Z"
203             })
204             .onGet("https://xc59z.example.com/discovery/v1/apis/arvados/v1/rest")
205             .reply(200, {
206                 baseUrl: "https://xc59z.example.com/arvados/v1",
207                 keepWebServiceUrl: "",
208                 keepWebInlineServiceUrl: "",
209                 remoteHosts: {},
210                 rootUrl: "https://xc59z.example.com",
211                 uuidPrefix: "xc59z",
212                 websocketUrl: "",
213                 workbenchUrl: "",
214                 workbench2Url: "",
215                 revision: 12345678
216             });
217
218         importMocks.push(ImportMock.mockFunction(servicesModule, 'createServices', services));
219
220         // Only test the case when a link account operation is not being cancelled
221         sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
222         localStorage.setItem(API_TOKEN_KEY, "token");
223
224         const config: any = {
225             rootUrl: "https://zzzzz.example.com",
226             uuidPrefix: "zzzzz",
227             remoteHosts: { xc59z: "xc59z.example.com" },
228             apiRevision: 12345678,
229             clusterConfig: {
230                 Login: { LoginCluster: "" },
231                 Workbench: { UserProfileFormFields: {} }
232             },
233         };
234
235         store.dispatch(initAuth(config));
236
237         store.subscribe(() => {
238             const auth = store.getState().auth;
239             if (auth.apiToken === "token" &&
240                 auth.sessions.length === 2 &&
241                 auth.sessions[0].status === SessionStatus.VALIDATED &&
242                 auth.sessions[1].status === SessionStatus.VALIDATED
243             ) {
244                 try {
245                     expect(auth).toEqual({
246                         apiToken: "token",
247                         apiTokenExpiration: new Date("2140-01-01T00:00:00.000Z"),
248                         apiTokenLocation: "localStorage",
249                         config: {
250                             apiRevision: 12345678,
251                             clusterConfig: {
252                                 Login: {
253                                     LoginCluster: "",
254                                 },
255                                 Workbench: { UserProfileFormFields: {} }
256                             },
257                             remoteHosts: {
258                                 "xc59z": "xc59z.example.com",
259                             },
260                             rootUrl: "https://zzzzz.example.com",
261                             uuidPrefix: "zzzzz",
262                         },
263                         sshKeys: [],
264                         extraApiToken: undefined,
265                         extraApiTokenExpiration: undefined,
266                         homeCluster: "zzzzz",
267                         localCluster: "zzzzz",
268                         loginCluster: undefined,
269                         remoteHostsConfig: {
270                             "zzzzz": {
271                                 "apiRevision": 12345678,
272                                 "clusterConfig": {
273                                     "Login": {
274                                         "LoginCluster": "",
275                                     },
276                                     Workbench: { UserProfileFormFields: {} }
277                                 },
278                                 "remoteHosts": {
279                                     "xc59z": "xc59z.example.com",
280                                 },
281                                 "rootUrl": "https://zzzzz.example.com",
282                                 "uuidPrefix": "zzzzz",
283                             },
284                             "xc59z": mockConfig({
285                                 apiRevision: 12345678,
286                                 baseUrl: "https://xc59z.example.com/arvados/v1",
287                                 rootUrl: "https://xc59z.example.com",
288                                 uuidPrefix: "xc59z"
289                             })
290                         },
291                         remoteHosts: {
292                             zzzzz: "zzzzz.example.com",
293                             xc59z: "xc59z.example.com"
294                         },
295                         sessions: [{
296                             "active": true,
297                             "baseUrl": undefined,
298                             "clusterId": "zzzzz",
299                             "email": "test@test.com",
300                             "loggedIn": true,
301                             "remoteHost": "https://zzzzz.example.com",
302                             "status": 2,
303                             "token": "token",
304                             "name": "John Doe",
305                             "apiRevision": 12345678,
306                             "uuid": "zzzzz-tpzed-abcefg",
307                             "userIsActive": true
308                         }, {
309                             "active": false,
310                             "baseUrl": "",
311                             "clusterId": "xc59z",
312                             "email": "",
313                             "loggedIn": false,
314                             "remoteHost": "xc59z.example.com",
315                             "status": 2,
316                             "token": "",
317                             "name": "",
318                             "uuid": "",
319                             "apiRevision": 0,
320                         }],
321                         user: {
322                             email: "test@test.com",
323                             firstName: "John",
324                             lastName: "Doe",
325                             uuid: "zzzzz-tpzed-abcefg",
326                             ownerUuid: "ownerUuid",
327                             username: "jdoe",
328                             prefs: { profile: {} },
329                             isAdmin: false,
330                             isActive: true
331                         }
332                     });
333                     done();
334                 } catch (e) {
335                     fail(e);
336                 }
337             }
338         });
339     });
340
341
342     // TODO: Add remaining action tests
343     /*
344        it('should fire external url to login', () => {
345        const initialState = undefined;
346        window.location.assign = jest.fn();
347        reducer(initialState, authActions.LOGIN());
348        expect(window.location.assign).toBeCalledWith(
349        `/login?return_to=${window.location.protocol}//${window.location.host}/token`
350        );
351        });
352
353        it('should fire external url to logout', () => {
354        const initialState = undefined;
355        window.location.assign = jest.fn();
356        reducer(initialState, authActions.LOGOUT());
357        expect(window.location.assign).toBeCalledWith(
358        `/logout?return_to=${location.protocol}//${location.host}`
359        );
360        });
361      */
362 });