Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,9 @@ declare namespace admin.projectManagement {

listAndroidApps(): Promise<admin.projectManagement.AndroidApp[]>;
listIosApps(): Promise<admin.projectManagement.IosApp[]>;
listAppMetadata(): Promise<admin.projectManagement.AppMetadata[]>;
androidApp(appId: string): admin.projectManagement.AndroidApp;
setDisplayName(newDisplayName: string): Promise<void>;
iosApp(appId: string): admin.projectManagement.IosApp;
shaCertificate(shaHash: string): admin.projectManagement.ShaCertificate;
createAndroidApp(
Expand Down
18 changes: 9 additions & 9 deletions src/project-management/app-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,19 @@ export enum AppPlatform {
}

export interface AppMetadata {
readonly appId: string;
readonly displayName?: string;
readonly platform: AppPlatform;
readonly projectId: string;
readonly resourceName: string;
appId: string;
displayName?: string;
platform: AppPlatform;
projectId: string;
resourceName: string;
}

export interface AndroidAppMetadata extends AppMetadata {
readonly platform: AppPlatform.ANDROID;
readonly packageName: string;
platform: AppPlatform.ANDROID;
packageName: string;
}

export interface IosAppMetadata extends AppMetadata {
readonly platform: AppPlatform.IOS;
readonly bundleId: string;
platform: AppPlatform.IOS;
bundleId: string;
}
12 changes: 12 additions & 0 deletions src/project-management/project-management-api-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ export class ProjectManagementRequestHandler {
'v1beta1');
}

/**
* @param {string} parentResourceName Fully-qualified resource name of the project whose iOS apps
* you want to list.
*/
public listAppMetadata(parentResourceName: string): Promise<object> {
return this.invokeRequestHandler(
'GET',
`${parentResourceName}:searchApps?page_size=${LIST_APPS_MAX_PAGE_SIZE}`,
/* requestData */ null,
'v1beta1');
}

/**
* @param {string} parentResourceName Fully-qualified resource name of the project that you want
* to create the Android app within.
Expand Down
64 changes: 49 additions & 15 deletions src/project-management/project-management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import * as validator from '../utils/validator';
import { AndroidApp, ShaCertificate } from './android-app';
import { IosApp } from './ios-app';
import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request';
import { AppMetadata } from './app-metadata';
import { AppMetadata, AppPlatform } from './app-metadata';

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

/**
* Update display name of the project
*/
public setDisplayName(displayName: string): Promise<void> {
throw new FirebaseProjectManagementError(
'service-unavailable', 'This service is not available');
public setDisplayName(newDisplayName: string): Promise<void> {
return this.requestHandler.setDisplayName(this.resourceName, newDisplayName);
}

private transformResponseToAppMetadata(responseData: any): AppMetadata[] {
this.assertListAppsResponseData(responseData, 'listAppMetadata()');

if (!responseData.apps) {
return [];
}

return responseData.apps.map((appJson: any) => {
assertServerResponse(
validator.isNonEmptyString(appJson.appId),
responseData,
`"apps[].appId" field must be present in the listAppMetadata() response data.`);
assertServerResponse(
validator.isNonEmptyString(appJson.platform),
responseData,
`"apps[].platform" field must be present in the listAppMetadata() response data.`);
const metadata: AppMetadata = {
appId: appJson.appId,
platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN,
projectId: this.projectId,
resourceName: appJson.name,
};
if (appJson.displayName) {
metadata.displayName = appJson.displayName;
}
return metadata;
});
}

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

return listPromise
.then((responseData: any) => {
assertServerResponse(
validator.isNonNullObject(responseData),
responseData,
`${callerName}\'s responseData must be a non-null object.`);
this.assertListAppsResponseData(responseData, callerName);

if (!responseData.apps) {
return [];
}

assertServerResponse(
validator.isArray(responseData.apps),
responseData,
`"apps" field must be present in the ${callerName} response data.`);

return responseData.apps.map((appJson: any) => {
assertServerResponse(
validator.isNonEmptyString(appJson.appId),
Expand All @@ -201,4 +221,18 @@ export class ProjectManagement implements FirebaseServiceInterface {
});
});
}

private assertListAppsResponseData(responseData: any, callerName: string): void {
assertServerResponse(
validator.isNonNullObject(responseData),
responseData,
`${callerName}\'s responseData must be a non-null object.`);

if (responseData.apps) {
assertServerResponse(
validator.isArray(responseData.apps),
responseData,
`"apps" field must be present in the ${callerName} response data.`);
}
}
}
42 changes: 42 additions & 0 deletions test/integration/project-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,25 @@

import * as _ from 'lodash';
import * as chai from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as admin from '../../lib/index';
import { projectId } from './setup';

const APP_NAMESPACE_PREFIX = 'com.adminsdkintegrationtest.a';
const APP_NAMESPACE_SUFFIX_LENGTH = 15;

const APP_DISPLAY_NAME_PREFIX = 'Created By Firebase AdminSDK Nodejs Integration Testing ';
const PROJECT_DISPLAY_NAME_PREFIX = 'Nodejs AdminSDK Testing ';
const APP_DISPLAY_NAME_SUFFIX_LENGTH = 15;
const PROJECT_DISPLAY_NAME_SUFFIX_LENGTH = 6;

const SHA_256_HASH = 'aaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaaccccaaaacccc';

const expect = chai.expect;

chai.should();
chai.use(chaiAsPromised);

describe('admin.projectManagement', () => {

let androidApp: admin.projectManagement.AndroidApp;
Expand Down Expand Up @@ -75,6 +81,28 @@ describe('admin.projectManagement', () => {
});
});

describe('setDisplayName()', () => {
it('successfully set project\'s display name', () => {
const newDisplayName = generateUniqueProjectDisplayName();
// TODO(caot): verify that project name has been renamed successfully
return admin.projectManagement().setDisplayName(newDisplayName)
.should.eventually.be.fulfilled;
});
});

describe('listAppMetadata()', () => {
it('successfully lists metadata of all apps', () => {
return admin.projectManagement().listAppMetadata()
.then((metadatas) => {
expect(metadatas.length).to.be.at.least(2);
const testAppMetadatas = metadatas.filter((metadata) =>
isIntegrationTestAppDisplayName(metadata.displayName) &&
(metadata.appId === androidApp.appId || metadata.appId === iosApp.appId));
expect(testAppMetadatas).to.have.length(2);
});
});
});

describe('androidApp.getMetadata()', () => {
it('successfully sets Android app\'s display name', () => {
return androidApp.getMetadata().then((appMetadata) => {
Expand Down Expand Up @@ -234,13 +262,27 @@ function generateUniqueAppDisplayName() {
return APP_DISPLAY_NAME_PREFIX + generateRandomString(APP_DISPLAY_NAME_SUFFIX_LENGTH);
}

/**
* @return {string} string that can be used as a unique project display name.
*/
function generateUniqueProjectDisplayName() {
return PROJECT_DISPLAY_NAME_PREFIX + generateRandomString(PROJECT_DISPLAY_NAME_SUFFIX_LENGTH);
}

/**
* @return {boolean} True if the specified appNamespace belongs to these integration tests.
*/
function isIntegrationTestApp(appNamespace: string): boolean {
return (appNamespace.indexOf(APP_NAMESPACE_PREFIX) > -1);
}

/**
* @return {boolean} True if the specified appDisplayName belongs to these integration tests.
*/
function isIntegrationTestAppDisplayName(appDisplayName: string): boolean {
return appDisplayName && (appDisplayName.indexOf(APP_DISPLAY_NAME_PREFIX) > -1);
}

/**
* @return {string} A randomly generated alphanumeric string, of the specified length.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { HttpClient } from '../../../src/utils/api-request';
import * as mocks from '../../resources/mocks';
import * as utils from '../utils';
import { ShaCertificate } from '../../../src/project-management/android-app';
import { AppPlatform } from '../../../src/project-management/app-metadata';

chai.should();
chai.use(sinonChai);
Expand All @@ -41,11 +42,15 @@ describe('ProjectManagementRequestHandler', () => {
const PORT = 443;
const PROJECT_RESOURCE_NAME: string = 'projects/test-project-id';
const APP_ID: string = 'test-app-id';
const APP_ID_ANDROID: string = 'test-android-app-id';
const APP_ID_IOS: string = 'test-ios-app-id';
const ANDROID_APP_RESOURCE_NAME: string = `projects/-/androidApp/${APP_ID}`;
const IOS_APP_RESOURCE_NAME: string = `projects/-/iosApp/${APP_ID}`;
const PACKAGE_NAME: string = 'test-package-name';
const BUNDLE_ID: string = 'test-bundle-id';
const DISPLAY_NAME: string = 'test-display-name';
const DISPLAY_NAME_ANDROID: string = 'test-display-name-android';
const DISPLAY_NAME_IOS: string = 'test-display-name-ios';
const OPERATION_RESOURCE_NAME: string = 'test-operation-resource-name';

const mockAccessToken: string = utils.generateRandomAccessToken();
Expand Down Expand Up @@ -183,6 +188,44 @@ describe('ProjectManagementRequestHandler', () => {
});
});

describe('listAppMetadata', () => {
testHttpErrors(() => requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME));

it('should succeed', () => {
const expectedResult = {
apps: [
{
appId: APP_ID_ANDROID,
displayName: DISPLAY_NAME_ANDROID,
platform: AppPlatform.ANDROID,
},
{
appId: APP_ID_IOS,
displayName: DISPLAY_NAME_IOS,
platform: AppPlatform.IOS,
}],
};

const stub = sinon.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom(expectedResult));
stubs.push(stub);

const url =
`https://${HOST}:${PORT}/v1beta1/${PROJECT_RESOURCE_NAME}:searchApps?page_size=100`;
return requestHandler.listAppMetadata(PROJECT_RESOURCE_NAME)
.then((result) => {
expect(result).to.deep.equal(expectedResult);
expect(stub).to.have.been.calledOnce.and.calledWith({
method: 'GET',
url,
data: null,
headers: expectedHeaders,
timeout: 10000,
});
});
});
});

describe('createAndroidApp', () => {
testHttpErrors(() => requestHandler.createAndroidApp(PROJECT_RESOURCE_NAME, PACKAGE_NAME));

Expand Down
Loading