Skip to content

Commit 807bac3

Browse files
authored
Implement listAppMetadata() and setDisplayName() of ProjectManagement class (#546)
* Added AppMetadata type * Declare setDisplayName and listAppMetadata (unimplemented) * Declare listAppMetadata and setDisplayName methods in Project Management class (#12) * Added AppMetadata type * Declare setDisplayName and listAppMetadata (unimplemented) * Update AndroidAppMetadata and IosAppMetadata * Change AppMetadata to interface and displayName to be optional * listAppMetadata: implement api request * listAppMetadata: implement listAppMetadata in ProjectManagement class (#16) * listAppMetadata: implement listAppMetadata in ProjectManagement class * Clean up code to address comments * Fix: non ios/android apps should be listed as PLATFORM_UNKNOWN (#18) * Add integration test for ProjectManagement.listAppMetadata() (#17) * Add integration test * Fix listAppMetadata integration test * Implement setDisplayName() for ProjectManagement class (#19) * Implement setDisplayName() for ProjectManagement class * fix integration test to address comment * Enable promise support for integration test * Fix listAppMetadata to match the update on AppMetatdata interface * Handle nullable displayName and enum string mapping issues * Update test for null display name
1 parent 1ff2e66 commit 807bac3

File tree

7 files changed

+341
-34
lines changed

7 files changed

+341
-34
lines changed

src/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,9 @@ declare namespace admin.projectManagement {
809809

810810
listAndroidApps(): Promise<admin.projectManagement.AndroidApp[]>;
811811
listIosApps(): Promise<admin.projectManagement.IosApp[]>;
812+
listAppMetadata(): Promise<admin.projectManagement.AppMetadata[]>;
812813
androidApp(appId: string): admin.projectManagement.AndroidApp;
814+
setDisplayName(newDisplayName: string): Promise<void>;
813815
iosApp(appId: string): admin.projectManagement.IosApp;
814816
shaCertificate(shaHash: string): admin.projectManagement.ShaCertificate;
815817
createAndroidApp(

src/project-management/app-metadata.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,19 @@ export enum AppPlatform {
2121
}
2222

2323
export interface AppMetadata {
24-
readonly appId: string;
25-
readonly displayName?: string;
26-
readonly platform: AppPlatform;
27-
readonly projectId: string;
28-
readonly resourceName: string;
24+
appId: string;
25+
displayName?: string;
26+
platform: AppPlatform;
27+
projectId: string;
28+
resourceName: string;
2929
}
3030

3131
export interface AndroidAppMetadata extends AppMetadata {
32-
readonly platform: AppPlatform.ANDROID;
33-
readonly packageName: string;
32+
platform: AppPlatform.ANDROID;
33+
packageName: string;
3434
}
3535

3636
export interface IosAppMetadata extends AppMetadata {
37-
readonly platform: AppPlatform.IOS;
38-
readonly bundleId: string;
37+
platform: AppPlatform.IOS;
38+
bundleId: string;
3939
}

src/project-management/project-management-api-request.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ export class ProjectManagementRequestHandler {
139139
'v1beta1');
140140
}
141141

142+
/**
143+
* @param {string} parentResourceName Fully-qualified resource name of the project whose iOS apps
144+
* you want to list.
145+
*/
146+
public listAppMetadata(parentResourceName: string): Promise<object> {
147+
return this.invokeRequestHandler(
148+
'GET',
149+
`${parentResourceName}:searchApps?page_size=${LIST_APPS_MAX_PAGE_SIZE}`,
150+
/* requestData */ null,
151+
'v1beta1');
152+
}
153+
142154
/**
143155
* @param {string} parentResourceName Fully-qualified resource name of the project that you want
144156
* to create the Android app within.

src/project-management/project-management.ts

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import * as validator from '../utils/validator';
2222
import { AndroidApp, ShaCertificate } from './android-app';
2323
import { IosApp } from './ios-app';
2424
import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request';
25-
import { AppMetadata } from './app-metadata';
25+
import { AppMetadata, AppPlatform } from './app-metadata';
2626

2727
/**
2828
* Internals of a Project Management instance.
@@ -152,16 +152,44 @@ export class ProjectManagement implements FirebaseServiceInterface {
152152
* Lists summary of all apps in the project
153153
*/
154154
public listAppMetadata(): Promise<AppMetadata[]> {
155-
throw new FirebaseProjectManagementError(
156-
'service-unavailable', 'This service is not available');
155+
return this.requestHandler.listAppMetadata(this.resourceName)
156+
.then((responseData) => this.transformResponseToAppMetadata(responseData));
157157
}
158158

159159
/**
160160
* Update display name of the project
161161
*/
162-
public setDisplayName(displayName: string): Promise<void> {
163-
throw new FirebaseProjectManagementError(
164-
'service-unavailable', 'This service is not available');
162+
public setDisplayName(newDisplayName: string): Promise<void> {
163+
return this.requestHandler.setDisplayName(this.resourceName, newDisplayName);
164+
}
165+
166+
private transformResponseToAppMetadata(responseData: any): AppMetadata[] {
167+
this.assertListAppsResponseData(responseData, 'listAppMetadata()');
168+
169+
if (!responseData.apps) {
170+
return [];
171+
}
172+
173+
return responseData.apps.map((appJson: any) => {
174+
assertServerResponse(
175+
validator.isNonEmptyString(appJson.appId),
176+
responseData,
177+
`"apps[].appId" field must be present in the listAppMetadata() response data.`);
178+
assertServerResponse(
179+
validator.isNonEmptyString(appJson.platform),
180+
responseData,
181+
`"apps[].platform" field must be present in the listAppMetadata() response data.`);
182+
const metadata: AppMetadata = {
183+
appId: appJson.appId,
184+
platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN,
185+
projectId: this.projectId,
186+
resourceName: appJson.name,
187+
};
188+
if (appJson.displayName) {
189+
metadata.displayName = appJson.displayName;
190+
}
191+
return metadata;
192+
});
165193
}
166194

167195
/**
@@ -174,20 +202,12 @@ export class ProjectManagement implements FirebaseServiceInterface {
174202

175203
return listPromise
176204
.then((responseData: any) => {
177-
assertServerResponse(
178-
validator.isNonNullObject(responseData),
179-
responseData,
180-
`${callerName}\'s responseData must be a non-null object.`);
205+
this.assertListAppsResponseData(responseData, callerName);
181206

182207
if (!responseData.apps) {
183208
return [];
184209
}
185210

186-
assertServerResponse(
187-
validator.isArray(responseData.apps),
188-
responseData,
189-
`"apps" field must be present in the ${callerName} response data.`);
190-
191211
return responseData.apps.map((appJson: any) => {
192212
assertServerResponse(
193213
validator.isNonEmptyString(appJson.appId),
@@ -201,4 +221,18 @@ export class ProjectManagement implements FirebaseServiceInterface {
201221
});
202222
});
203223
}
224+
225+
private assertListAppsResponseData(responseData: any, callerName: string): void {
226+
assertServerResponse(
227+
validator.isNonNullObject(responseData),
228+
responseData,
229+
`${callerName}\'s responseData must be a non-null object.`);
230+
231+
if (responseData.apps) {
232+
assertServerResponse(
233+
validator.isArray(responseData.apps),
234+
responseData,
235+
`"apps" field must be present in the ${callerName} response data.`);
236+
}
237+
}
204238
}

test/integration/project-management.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,25 @@
1616

1717
import * as _ from 'lodash';
1818
import * as chai from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
1920
import * as admin from '../../lib/index';
2021
import { projectId } from './setup';
2122

2223
const APP_NAMESPACE_PREFIX = 'com.adminsdkintegrationtest.a';
2324
const APP_NAMESPACE_SUFFIX_LENGTH = 15;
2425

2526
const APP_DISPLAY_NAME_PREFIX = 'Created By Firebase AdminSDK Nodejs Integration Testing ';
27+
const PROJECT_DISPLAY_NAME_PREFIX = 'Nodejs AdminSDK Testing ';
2628
const APP_DISPLAY_NAME_SUFFIX_LENGTH = 15;
29+
const PROJECT_DISPLAY_NAME_SUFFIX_LENGTH = 6;
2730

2831
const SHA_256_HASH = 'aaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaacccc';
2932

3033
const expect = chai.expect;
3134

35+
chai.should();
36+
chai.use(chaiAsPromised);
37+
3238
describe('admin.projectManagement', () => {
3339

3440
let androidApp: admin.projectManagement.AndroidApp;
@@ -75,6 +81,28 @@ describe('admin.projectManagement', () => {
7581
});
7682
});
7783

84+
describe('setDisplayName()', () => {
85+
it('successfully set project\'s display name', () => {
86+
const newDisplayName = generateUniqueProjectDisplayName();
87+
// TODO(caot): verify that project name has been renamed successfully
88+
return admin.projectManagement().setDisplayName(newDisplayName)
89+
.should.eventually.be.fulfilled;
90+
});
91+
});
92+
93+
describe('listAppMetadata()', () => {
94+
it('successfully lists metadata of all apps', () => {
95+
return admin.projectManagement().listAppMetadata()
96+
.then((metadatas) => {
97+
expect(metadatas.length).to.be.at.least(2);
98+
const testAppMetadatas = metadatas.filter((metadata) =>
99+
isIntegrationTestAppDisplayName(metadata.displayName) &&
100+
(metadata.appId === androidApp.appId || metadata.appId === iosApp.appId));
101+
expect(testAppMetadatas).to.have.length(2);
102+
});
103+
});
104+
});
105+
78106
describe('androidApp.getMetadata()', () => {
79107
it('successfully sets Android app\'s display name', () => {
80108
return androidApp.getMetadata().then((appMetadata) => {
@@ -234,13 +262,27 @@ function generateUniqueAppDisplayName() {
234262
return APP_DISPLAY_NAME_PREFIX + generateRandomString(APP_DISPLAY_NAME_SUFFIX_LENGTH);
235263
}
236264

265+
/**
266+
* @return {string} string that can be used as a unique project display name.
267+
*/
268+
function generateUniqueProjectDisplayName() {
269+
return PROJECT_DISPLAY_NAME_PREFIX + generateRandomString(PROJECT_DISPLAY_NAME_SUFFIX_LENGTH);
270+
}
271+
237272
/**
238273
* @return {boolean} True if the specified appNamespace belongs to these integration tests.
239274
*/
240275
function isIntegrationTestApp(appNamespace: string): boolean {
241276
return (appNamespace.indexOf(APP_NAMESPACE_PREFIX) > -1);
242277
}
243278

279+
/**
280+
* @return {boolean} True if the specified appDisplayName belongs to these integration tests.
281+
*/
282+
function isIntegrationTestAppDisplayName(appDisplayName: string): boolean {
283+
return appDisplayName && (appDisplayName.indexOf(APP_DISPLAY_NAME_PREFIX) > -1);
284+
}
285+
244286
/**
245287
* @return {string} A randomly generated alphanumeric string, of the specified length.
246288
*/

test/unit/project-management/project-management-api-request.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { HttpClient } from '../../../src/utils/api-request';
2727
import * as mocks from '../../resources/mocks';
2828
import * as utils from '../utils';
2929
import { ShaCertificate } from '../../../src/project-management/android-app';
30+
import { AppPlatform } from '../../../src/project-management/app-metadata';
3031

3132
chai.should();
3233
chai.use(sinonChai);
@@ -41,11 +42,15 @@ describe('ProjectManagementRequestHandler', () => {
4142
const PORT = 443;
4243
const PROJECT_RESOURCE_NAME: string = 'projects/test-project-id';
4344
const APP_ID: string = 'test-app-id';
45+
const APP_ID_ANDROID: string = 'test-android-app-id';
46+
const APP_ID_IOS: string = 'test-ios-app-id';
4447
const ANDROID_APP_RESOURCE_NAME: string = `projects/-/androidApp/${APP_ID}`;
4548
const IOS_APP_RESOURCE_NAME: string = `projects/-/iosApp/${APP_ID}`;
4649
const PACKAGE_NAME: string = 'test-package-name';
4750
const BUNDLE_ID: string = 'test-bundle-id';
4851
const DISPLAY_NAME: string = 'test-display-name';
52+
const DISPLAY_NAME_ANDROID: string = 'test-display-name-android';
53+
const DISPLAY_NAME_IOS: string = 'test-display-name-ios';
4954
const OPERATION_RESOURCE_NAME: string = 'test-operation-resource-name';
5055

5156
const mockAccessToken: string = utils.generateRandomAccessToken();
@@ -183,6 +188,44 @@ describe('ProjectManagementRequestHandler', () => {
183188
});
184189
});
185190

191+
describe('listAppMetadata', () => {
192+
testHttpErrors(() => requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME));
193+
194+
it('should succeed', () => {
195+
const expectedResult = {
196+
apps: [
197+
{
198+
appId: APP_ID_ANDROID,
199+
displayName: DISPLAY_NAME_ANDROID,
200+
platform: AppPlatform.ANDROID,
201+
},
202+
{
203+
appId: APP_ID_IOS,
204+
displayName: DISPLAY_NAME_IOS,
205+
platform: AppPlatform.IOS,
206+
}],
207+
};
208+
209+
const stub = sinon.stub(HttpClient.prototype, 'send')
210+
.resolves(utils.responseFrom(expectedResult));
211+
stubs.push(stub);
212+
213+
const url =
214+
`https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}:searchApps?page_size=100`;
215+
return requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME)
216+
.then((result) => {
217+
expect(result).to.deep.equal(expectedResult);
218+
expect(stub).to.have.been.calledOnce.and.calledWith({
219+
method: 'GET',
220+
url,
221+
data: null,
222+
headers: expectedHeaders,
223+
timeout: 10000,
224+
});
225+
});
226+
});
227+
});
228+
186229
describe('createAndroidApp', () => {
187230
testHttpErrors(() => requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME));
188231

0 commit comments

Comments
 (0)