@forge/migrations 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ import { APIError, migration } from '../migration';
2
+ import { Progress } from '../migration/migration-adaptor';
3
+ import { when } from 'jest-when';
4
+ import fetch, { Response } from 'node-fetch';
5
+ import { mocked } from 'ts-jest/utils';
6
+
7
+ let transferId: string;
8
+ let invalidTransferId: string;
9
+ let progress: Progress;
10
+ let keys: string[];
11
+ let invalidKeys: string[];
12
+ let namespace: string;
13
+ let mappingByKeyResponseWithKeys: string;
14
+ let mappingByKeyResponseWithInvalidKeys: string;
15
+ let s3Key: string;
16
+ let invalidS3Key: string;
17
+ let feedbackPayload: object;
18
+ let appDataListResponse: string;
19
+ let appDataPayloadResponse: string;
20
+ jest.mock('node-fetch');
21
+
22
+ describe('migration', () => {
23
+ beforeAll(() => {
24
+ (global as any).api = {
25
+ asApp: jest.fn().mockReturnValue({
26
+ __requestAtlassian: jest.fn()
27
+ })
28
+ };
29
+ transferId = '521ee36a-b673-3dc6-b60b-12460332c200';
30
+ invalidTransferId = '521ee36a-b673-3dc6-b60b-12460332c001';
31
+ });
32
+
33
+ describe('sendMigrationProgress', () => {
34
+ beforeAll(() => {
35
+ progress = { status: 'IN_PROGRESS', percent: 10, message: 'reporting' };
36
+ when((global as any).api.asApp().__requestAtlassian)
37
+ .calledWith(`/app/migration/forge/v1/progress/${transferId}`, {
38
+ method: 'POST',
39
+ headers: {
40
+ 'content-type': 'application/json'
41
+ },
42
+ body: JSON.stringify(progress)
43
+ })
44
+ .mockReturnValue({
45
+ status: 200,
46
+ text: jest.fn().mockImplementationOnce(() => Promise.resolve()),
47
+ json: jest.fn().mockImplementationOnce(() => Promise.resolve())
48
+ })
49
+ .calledWith(`/app/migration/forge/v1/progress/${invalidTransferId}`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'content-type': 'application/json'
53
+ },
54
+ body: JSON.stringify(progress)
55
+ })
56
+ .mockReturnValue({
57
+ status: 403,
58
+ statusText: 'Forbidden'
59
+ });
60
+ });
61
+
62
+ it('should update progress when sendProgress method called with valid transferId', async () => {
63
+ const returnedValue = migration.sendProgress(transferId, progress);
64
+ await expect(returnedValue).resolves.toBeUndefined();
65
+ });
66
+
67
+ it('should return 403 when sendProgress method called with invalid transferId', async () => {
68
+ const returnedValue = migration.sendProgress(invalidTransferId, progress);
69
+ await expect(returnedValue).rejects.toThrow(APIError.forStatus(403, 'Forbidden'));
70
+ });
71
+ });
72
+
73
+ describe('getMappingById', () => {
74
+ beforeAll(() => {
75
+ keys = ['email/abc@example.com', 'email/admin@example.com'];
76
+ invalidKeys = ['email/abc@example.com', 'email/admin@example'];
77
+ namespace = 'identity:user';
78
+ mappingByKeyResponseWithKeys =
79
+ '{"email/abc@example.com":"5a20a4d7c6bd4a32df3a711","email/admin@example.com":"5a29c4ae20cfc31b0dc0e112"}';
80
+ mappingByKeyResponseWithInvalidKeys = '{"email/abc@example.com":"5a20a4d7c6bd4a32df3a711"}';
81
+
82
+ when((global as any).api.asApp().__requestAtlassian)
83
+ .calledWith(`/app/migration/forge/v1/mapping/${transferId}/find?namespace=${namespace}`, {
84
+ method: 'POST',
85
+ headers: {
86
+ 'content-type': 'application/json'
87
+ },
88
+ body: JSON.stringify(keys)
89
+ })
90
+ .mockReturnValue({
91
+ status: 200,
92
+ text: jest.fn().mockResolvedValue(mappingByKeyResponseWithKeys),
93
+ json: jest.fn().mockResolvedValue(JSON.parse(mappingByKeyResponseWithKeys))
94
+ })
95
+ .calledWith(`/app/migration/forge/v1/mapping/${transferId}/find?namespace=${namespace}`, {
96
+ method: 'POST',
97
+ headers: {
98
+ 'content-type': 'application/json'
99
+ },
100
+ body: JSON.stringify(invalidKeys)
101
+ })
102
+ .mockReturnValue({
103
+ status: 200,
104
+ text: jest.fn().mockResolvedValue(mappingByKeyResponseWithInvalidKeys),
105
+ json: jest.fn().mockResolvedValue(JSON.parse(mappingByKeyResponseWithInvalidKeys))
106
+ })
107
+ .calledWith(`/app/migration/forge/v1/mapping/${invalidTransferId}/find?namespace=${namespace}`, {
108
+ method: 'POST',
109
+ headers: {
110
+ 'content-type': 'application/json'
111
+ },
112
+ body: JSON.stringify(keys)
113
+ })
114
+ .mockReturnValue({
115
+ status: 403,
116
+ statusText: 'Forbidden'
117
+ });
118
+ });
119
+
120
+ it('should return MappingResponse when getMappingById method called with valid transferId', async () => {
121
+ const returnedValue = await migration.getMappingById(transferId, namespace, keys);
122
+ expect(JSON.stringify(returnedValue)).toEqual(mappingByKeyResponseWithKeys);
123
+ });
124
+
125
+ it('should return 403 when getMappingById method called with invalid transferId', async () => {
126
+ const returnedValue = migration.getMappingById(invalidTransferId, namespace, keys);
127
+ await expect(returnedValue).rejects.toThrow(APIError.forStatus(403, 'Forbidden'));
128
+ });
129
+
130
+ it('should return MappingResponse except for invalid keys when getMappingById method called with valid transferId', async () => {
131
+ const returnedValue = await migration.getMappingById(transferId, namespace, invalidKeys);
132
+ expect(JSON.stringify(returnedValue)).toEqual(mappingByKeyResponseWithInvalidKeys);
133
+ });
134
+ });
135
+
136
+ describe('getAppDataList', () => {
137
+ beforeAll(() => {
138
+ appDataListResponse =
139
+ '[{"s3Key":"2ea231e3-ab0d-4236-97f7-26f951df1c11","label":null},{"s3Key":"317a3631-2fef-4940-a418-7096e971eb11","label":null}]';
140
+
141
+ when((global as any).api.asApp().__requestAtlassian)
142
+ .calledWith(`/app/migration/forge/v1/data/${transferId}/all`)
143
+ .mockReturnValue({
144
+ status: 200,
145
+ text: jest.fn().mockResolvedValue(appDataListResponse),
146
+ json: jest.fn().mockResolvedValue(JSON.parse(appDataListResponse))
147
+ })
148
+ .calledWith(`/app/migration/forge/v1/data/${invalidTransferId}/all`)
149
+ .mockReturnValue({
150
+ status: 403,
151
+ statusText: 'Forbidden'
152
+ });
153
+ });
154
+
155
+ it('should return AppDataListResponse when getAppDataList method called with valid transferId', async () => {
156
+ const returnedValue = await migration.getAppDataList(transferId);
157
+ expect(returnedValue).toEqual(JSON.parse(appDataListResponse));
158
+ });
159
+
160
+ it('should return 403 when getAppDataList method called with invalid transferId', async () => {
161
+ const returnedValue = migration.getAppDataList(invalidTransferId);
162
+ await expect(returnedValue).rejects.toThrow(APIError.forStatus(403, 'Forbidden'));
163
+ });
164
+ });
165
+
166
+ describe('getAppDataPayload', () => {
167
+ beforeAll(() => {
168
+ s3Key = '7941b65d-094b-4305-97f2-e2b412c6db0a';
169
+ invalidS3Key = '7941b65d-094b-4305-97f2-e2b412c000';
170
+ appDataPayloadResponse = '{ "url": "https://rps--stg-east--app-migration-service--ams.s3.amazonaws.com/test"}';
171
+
172
+ mocked(fetch).mockImplementation(() =>
173
+ Promise.resolve({
174
+ text: () => Promise.resolve('Sample app data')
175
+ } as Response)
176
+ );
177
+
178
+ when((global as any).api.asApp().__requestAtlassian)
179
+ .calledWith(`/app/migration/forge/v1/data/${s3Key}`)
180
+ .mockReturnValue({
181
+ status: 200,
182
+ text: jest.fn().mockResolvedValue(JSON.stringify(appDataPayloadResponse)),
183
+ json: jest.fn().mockResolvedValue(JSON.parse(appDataPayloadResponse))
184
+ })
185
+ .calledWith(`/app/migration/forge/v1/data/${invalidS3Key}`)
186
+ .mockReturnValue({
187
+ status: 403,
188
+ statusText: 'Forbidden'
189
+ });
190
+ });
191
+
192
+ it('should return Response when getAppDataPayload method called with valid S3Key', async () => {
193
+ const returnedValue = await migration.getAppDataPayload(s3Key);
194
+ await expect(returnedValue.text()).resolves.toBe('Sample app data');
195
+ });
196
+
197
+ it('should return 403 when getAppDataPayload method called with invalid S3Key', async () => {
198
+ const returnedValue = migration.getAppDataPayload(invalidS3Key);
199
+ await expect(returnedValue).rejects.toThrow(APIError.forStatus(403, 'Forbidden'));
200
+ });
201
+ });
202
+
203
+ describe('updateFeedback', () => {
204
+ beforeAll(() => {
205
+ feedbackPayload = { transferId: transferId, msg: 'Current Data processed, send next' };
206
+ when((global as any).api.asApp().__requestAtlassian)
207
+ .calledWith(`/app/migration/forge/v1/feedback/${transferId}`, {
208
+ method: 'POST',
209
+ headers: {
210
+ 'content-type': 'application/json'
211
+ },
212
+ body: JSON.stringify(feedbackPayload)
213
+ })
214
+ .mockReturnValue({
215
+ status: 200,
216
+ text: jest.fn().mockImplementationOnce(() => Promise.resolve()),
217
+ json: jest.fn().mockImplementationOnce(() => Promise.resolve())
218
+ })
219
+ .calledWith(`/app/migration/forge/v1/feedback/${invalidTransferId}`, {
220
+ method: 'POST',
221
+ headers: {
222
+ 'content-type': 'application/json'
223
+ },
224
+ body: JSON.stringify(feedbackPayload)
225
+ })
226
+ .mockReturnValue({
227
+ status: 403,
228
+ statusText: 'Forbidden'
229
+ });
230
+ });
231
+
232
+ it('should send feedback when updateFeedback method called with valid transferId', async () => {
233
+ const returnedValue = migration.updateFeedback(transferId, feedbackPayload);
234
+ await expect(returnedValue).resolves.toBeUndefined();
235
+ });
236
+
237
+ it('should return 403 when updateFeedback method called with invalid transferId', async () => {
238
+ const returnedValue = migration.updateFeedback(invalidTransferId, feedbackPayload);
239
+ await expect(returnedValue).rejects.toThrow(APIError.forStatus(403, 'Forbidden'));
240
+ });
241
+ });
242
+ });
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { migration } from './migration';
2
+
3
+ export { migration };
@@ -0,0 +1,13 @@
1
+ export class APIError extends Error {
2
+ private constructor(message: string) {
3
+ super(message);
4
+ }
5
+
6
+ public static forStatus(status: number, message: string): APIError {
7
+ return new APIError(message);
8
+ }
9
+
10
+ public static forUnexpected(message: string): APIError {
11
+ return new APIError(message);
12
+ }
13
+ }
@@ -0,0 +1,26 @@
1
+ import { Migration } from './migration';
2
+ import { MigrationAdapter, Progress } from './migration-adaptor';
3
+
4
+ export { APIError } from './errors';
5
+
6
+ export const getMigrationInstance = (adapter: MigrationAdapter) => {
7
+ return {
8
+ sendProgress: (transferId: string, progress: Progress) => adapter.sendProgress(transferId, progress),
9
+ getMappingById: (transferId: string, namespace: string, keys: Array<string>) =>
10
+ adapter.getMappingById(transferId, namespace, keys),
11
+ updateFeedback: (transferId: string, feedback: object) => adapter.updateFeedback(transferId, feedback),
12
+ getAppDataList: (transferId: string) => adapter.getAppDataList(transferId),
13
+ getAppDataPayload: (s3Key: string) => adapter.getAppDataPayload(s3Key)
14
+ };
15
+ };
16
+
17
+ export const migration = {
18
+ sendProgress: (transferId: string, progress: Progress) =>
19
+ getMigrationInstance(new Migration()).sendProgress(transferId, progress),
20
+ getMappingById: (transferId: string, namespace: string, keys: Array<string>) =>
21
+ getMigrationInstance(new Migration()).getMappingById(transferId, namespace, keys),
22
+ updateFeedback: (transferId: string, feedback: object) =>
23
+ getMigrationInstance(new Migration()).updateFeedback(transferId, feedback),
24
+ getAppDataList: (transferId: string) => getMigrationInstance(new Migration()).getAppDataList(transferId),
25
+ getAppDataPayload: (s3Key: string) => getMigrationInstance(new Migration()).getAppDataPayload(s3Key)
26
+ };
@@ -0,0 +1,32 @@
1
+ import { Response } from 'node-fetch';
2
+
3
+ export interface MigrationAdapter {
4
+ sendProgress(transferId: string, progress: Progress): Promise<void>;
5
+ getMappingById(transferId: string, namespace: string, keys: Array<string>): Promise<MappingResponse>;
6
+ getAppDataList(transferId: string): Promise<AppDataListResponse>;
7
+ getAppDataPayload(s3Key: string): Promise<Response>;
8
+ updateFeedback(transferId: string, feedback: object): Promise<void>;
9
+ }
10
+
11
+ export interface MappingResponse {
12
+ result: Map<string, string>;
13
+ }
14
+
15
+ export interface AppData {
16
+ s3Key: string;
17
+ label: string;
18
+ }
19
+
20
+ export interface AppDataListResponse {
21
+ result: Set<AppData>;
22
+ }
23
+
24
+ export interface AppDataPayloadResponse {
25
+ url: string;
26
+ }
27
+
28
+ export interface Progress {
29
+ status: string;
30
+ percent: number;
31
+ message: string;
32
+ }
@@ -0,0 +1,41 @@
1
+ import fetch, { Response } from 'node-fetch';
2
+
3
+ import {
4
+ AppDataListResponse,
5
+ AppDataPayloadResponse,
6
+ MappingResponse,
7
+ MigrationAdapter,
8
+ Progress
9
+ } from './migration-adaptor';
10
+
11
+ import { getResponseBody, invokeGETApi, invokePOSTApi } from './utils';
12
+
13
+ export class Migration implements MigrationAdapter {
14
+ private readonly basePath = '/app/migration/forge/v1';
15
+
16
+ sendProgress = async (transferId: string, progress: Progress): Promise<void> => {
17
+ const result = await invokePOSTApi(`${this.basePath}/progress/${transferId}`, progress);
18
+ return getResponseBody(result);
19
+ };
20
+
21
+ getMappingById = async (transferId: string, namespace: string, keys: Array<string>): Promise<MappingResponse> => {
22
+ const result = await invokePOSTApi(`${this.basePath}/mapping/${transferId}/find?namespace=${namespace}`, keys);
23
+ return getResponseBody(result);
24
+ };
25
+
26
+ getAppDataList = async (transferId: string): Promise<AppDataListResponse> => {
27
+ const result = await invokeGETApi(`${this.basePath}/data/${transferId}/all`);
28
+ return getResponseBody(result);
29
+ };
30
+
31
+ getAppDataPayload = async (s3Key: string): Promise<Response> => {
32
+ const result = await invokeGETApi(`${this.basePath}/data/${s3Key}`);
33
+ const responseBody: AppDataPayloadResponse = await getResponseBody(result);
34
+ return fetch(responseBody.url);
35
+ };
36
+
37
+ updateFeedback = async (transferId: string, feedback: object): Promise<void> => {
38
+ const result = await invokePOSTApi(`${this.basePath}/feedback/${transferId}`, feedback);
39
+ return getResponseBody(result);
40
+ };
41
+ }
@@ -0,0 +1,31 @@
1
+ import { APIResponse } from '@forge/api';
2
+ import { APIError } from './errors';
3
+
4
+ export async function getResponseBody(response: APIResponse): Promise<any> {
5
+ if (response.status !== 200) {
6
+ throw APIError.forStatus(response.status, response.statusText);
7
+ }
8
+
9
+ const responseText = await response.text();
10
+ try {
11
+ if (responseText !== '' && responseText !== undefined) {
12
+ return JSON.parse(responseText);
13
+ }
14
+ } catch (error) {
15
+ throw APIError.forUnexpected(`Response text was not a valid JSON: ${responseText}`);
16
+ }
17
+ }
18
+
19
+ export async function invokePOSTApi(url: string, payload: any): Promise<APIResponse> {
20
+ return (global as any).api.asApp().__requestAtlassian(url, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'content-type': 'application/json'
24
+ },
25
+ body: JSON.stringify(payload)
26
+ });
27
+ }
28
+
29
+ export async function invokeGETApi(url: string): Promise<APIResponse> {
30
+ return (global as any).api.asApp().__requestAtlassian(url);
31
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./out",
5
+ "rootDir": "src",
6
+ "composite": true
7
+ },
8
+ "references": [
9
+ {
10
+ "path": "../forge-api"
11
+ }
12
+ ]
13
+ }