Skip to content
Closed
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
108 changes: 108 additions & 0 deletions spec/v2/providers/identity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,114 @@
};

describe("identity", () => {
describe("onUserCreated", () => {
it("should accept a handler", async () => {
let received: identity.UserRecord | undefined;
const fn = identity.onUserCreated((event) => {
received = event.data;
return null;
});

expect(fn.__endpoint).to.deep.equal({
...MINIMAL_V2_ENDPOINT,
platform: "gcfv2",
labels: {},
eventTrigger: {
eventType: identity.userCreatedEvent,
eventFilters: {},
retry: false,
},
});

const rawEvent = {
specversion: "1.0" as const,
id: "event-id",
source: "//identitytoolkit.googleapis.com/projects/project-id",
type: identity.userCreatedEvent,
time: new Date().toISOString(),
data: {
uid: "abc123",
metadata: {
creationTime: "2016-12-15T19:37:37.059Z",
lastSignInTime: "2017-01-01T00:00:00.000Z",
},
},
};

await fn(rawEvent as any);
expect(received).to.exist;
expect(received!.uid).to.equal("abc123");

Check failure on line 104 in spec/v2/providers/identity.spec.ts

View workflow job for this annotation

GitHub Actions / lint (22.x)

This assertion is unnecessary since it does not change the type of the expression
expect(received!.metadata).to.be.instanceof(identity.UserRecordMetadata);

Check failure on line 105 in spec/v2/providers/identity.spec.ts

View workflow job for this annotation

GitHub Actions / lint (22.x)

This assertion is unnecessary since it does not change the type of the expression
});

it("should accept options and a handler", () => {
const fn = identity.onUserCreated(
{
region: "us-central1",
minInstances: 2,
memory: "512MiB",
retry: true,
},
() => null
);

expect(fn.__endpoint).to.deep.equal({
...MINIMAL_V2_ENDPOINT,
platform: "gcfv2",
availableMemoryMb: 512,
region: ["us-central1"],
minInstances: 2,
labels: {},
eventTrigger: {
eventType: identity.userCreatedEvent,
eventFilters: {},
retry: true,
},
});
});
});

describe("onUserDeleted", () => {
it("should accept a handler", () => {
const fn = identity.onUserDeleted(() => null);

expect(fn.__endpoint).to.deep.equal({
...MINIMAL_V2_ENDPOINT,
platform: "gcfv2",
labels: {},
eventTrigger: {
eventType: identity.userDeletedEvent,
eventFilters: {},
retry: false,
},
});
});
Comment on lines +136 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The test for onUserDeleted only verifies the endpoint configuration. To improve test coverage and ensure the data transformation logic is correct for this trigger, this test should be made async and include assertions to check that the handler receives the correctly hydrated user data. This would align it with the more comprehensive test for onUserCreated.

    it("should accept a handler", async () => {
      let received: identity.UserRecord | undefined;
      const fn = identity.onUserDeleted((event) => {
        received = event.data;
        return null;
      });

      expect(fn.__endpoint).to.deep.equal({
        ...MINIMAL_V2_ENDPOINT,
        platform: "gcfv2",
        labels: {},
        eventTrigger: {
          eventType: identity.userDeletedEvent,
          eventFilters: {},
          retry: false,
        },
      });

      const rawEvent = {
        specversion: "1.0" as const,
        id: "event-id",
        source: "//identitytoolkit.googleapis.com/projects/project-id",
        type: identity.userDeletedEvent,
        time: new Date().toISOString(),
        data: {
          uid: "abc123",
          metadata: {
            creationTime: "2016-12-15T19:37:37.059Z",
            lastSignInTime: "2017-01-01T00:00:00.000Z",
          },
        },
      };

      await fn(rawEvent as any);
      expect(received).to.exist;
      expect(received!.uid).to.equal("abc123");
      expect(received!.metadata).to.be.instanceof(identity.UserRecordMetadata);
    });


it("should accept options and a handler", () => {
const fn = identity.onUserDeleted(
{
region: "europe-west3",
concurrency: 5,
retry: true,
},
() => null
);

expect(fn.__endpoint).to.deep.equal({
...MINIMAL_V2_ENDPOINT,
platform: "gcfv2",
concurrency: 5,
region: ["europe-west3"],
labels: {},
eventTrigger: {
eventType: identity.userDeletedEvent,
eventFilters: {},
retry: true,
},
});
});
});

describe("beforeUserCreated", () => {
it("should accept a handler", () => {
const fn = identity.beforeUserCreated(() => Promise.resolve());
Expand Down
135 changes: 133 additions & 2 deletions src/v2/providers/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,23 @@ import {
HttpsError,
wrapHandler,
MaybeAsync,
UserInfo,
UserRecord,
UserRecordMetadata,
userRecordConstructor,
} from "../../common/providers/identity";
import { BlockingFunction } from "../../v1/cloud-functions";
import { wrapTraceContext } from "../trace";
import { Expression } from "../../params";
import { initV2Endpoint } from "../../runtime/manifest";
import { CloudEvent, CloudFunction } from "../core";
import { initV2Endpoint, ManifestEndpoint } from "../../runtime/manifest";
import * as options from "../options";
import { SecretParam } from "../../params/types";
import { withInit } from "../../common/onInit";

export { HttpsError };
export type { AuthUserRecord, AuthBlockingEvent };
export { UserRecordMetadata, userRecordConstructor };
export type { AuthUserRecord, AuthBlockingEvent, UserRecord, UserInfo };

/** @hidden Internally used when parsing the options. */
interface InternalOptions {
Expand Down Expand Up @@ -166,6 +172,84 @@ export interface BlockingOptions {
secrets?: (string | SecretParam)[];
}

/** @internal */
export const userCreatedEvent = "google.firebase.auth.user.v1.created";
/** @internal */
export const userDeletedEvent = "google.firebase.auth.user.v1.deleted";

/** A CloudEvent that contains a Firebase Auth user record. */
export type UserRecordEvent = CloudEvent<UserRecord>;

type UserEventHandler = (event: UserRecordEvent) => any | Promise<any>;

/**
* Event handler which triggers when a Firebase Auth user is created.
*
* @param handler - Event handler which is run every time a Firebase Auth user is created.
* @returns A function that you can export and deploy.
*/
export function onUserCreated(handler: UserEventHandler): CloudFunction<UserRecordEvent>;

/**
* Event handler which triggers when a Firebase Auth user is created.
*
* @param opts - Options that can be set on an individual event-handling function.
* @param handler - Event handler which is run every time a Firebase Auth user is created.
* @returns A function that you can export and deploy.
*/
export function onUserCreated(
opts: options.EventHandlerOptions,
handler: UserEventHandler
): CloudFunction<UserRecordEvent>;

/**
* Event handler which triggers when a Firebase Auth user is created.
*
* @param optsOrHandler - Options or an event handler.
* @param handler - Event handler which is run every time a Firebase Auth user is created.
* @returns A function that you can export and deploy.
*/
export function onUserCreated(
optsOrHandler: options.EventHandlerOptions | UserEventHandler,
handler?: UserEventHandler
): CloudFunction<UserRecordEvent> {
return onUserOperation(userCreatedEvent, optsOrHandler, handler);
}

/**
* Event handler which triggers when a Firebase Auth user is deleted.
*
* @param handler - Event handler which is run every time a Firebase Auth user is deleted.
* @returns A function that you can export and deploy.
*/
export function onUserDeleted(handler: UserEventHandler): CloudFunction<UserRecordEvent>;

/**
* Event handler which triggers when a Firebase Auth user is deleted.
*
* @param opts - Options that can be set on an individual event-handling function.
* @param handler - Event handler which is run every time a Firebase Auth user is deleted.
* @returns A function that you can export and deploy.
*/
export function onUserDeleted(
opts: options.EventHandlerOptions,
handler: UserEventHandler
): CloudFunction<UserRecordEvent>;

/**
* Event handler which triggers when a Firebase Auth user is deleted.
*
* @param optsOrHandler - Options or an event handler.
* @param handler - Event handler which is run every time a Firebase Auth user is deleted.
* @returns A function that you can export and deploy.
*/
export function onUserDeleted(
optsOrHandler: options.EventHandlerOptions | UserEventHandler,
handler?: UserEventHandler
): CloudFunction<UserRecordEvent> {
return onUserOperation(userDeletedEvent, optsOrHandler, handler);
}

/**
* Handles an event that is triggered before a user is created.
* @param handler - Event handler which is run every time before a user is created.
Expand Down Expand Up @@ -293,6 +377,53 @@ export function beforeSmsSent(
return beforeOperation("beforeSendSms", optsOrHandler, handler);
}

function onUserOperation(
eventType: string,
optsOrHandler: options.EventHandlerOptions | UserEventHandler,
handler?: UserEventHandler
): CloudFunction<UserRecordEvent> {
if (typeof optsOrHandler === "function") {
handler = optsOrHandler;
optsOrHandler = {};
}

const baseOpts = options.optionsToEndpoint(options.getGlobalOptions());
const specificOpts = options.optionsToEndpoint(optsOrHandler);

const func: any = (raw: CloudEvent<unknown>) => {
const event = convertToUserRecordEvent(raw);
return wrapTraceContext(withInit(handler))(event);
};

func.run = handler;

const endpoint: ManifestEndpoint = {
...initV2Endpoint(options.getGlobalOptions(), optsOrHandler),
platform: "gcfv2",
...baseOpts,
...specificOpts,
labels: {
...baseOpts?.labels,
...specificOpts?.labels,
},
eventTrigger: {
eventType,
eventFilters: {},
retry: optsOrHandler.retry ?? false,
},
};

func.__endpoint = endpoint;

return func as CloudFunction<UserRecordEvent>;
}

function convertToUserRecordEvent(raw: CloudEvent<unknown>): UserRecordEvent {
const data = (raw.data ?? {}) as Record<string, unknown>;
const user = userRecordConstructor(data);
return { ...raw, data: user } as UserRecordEvent;
}

/** @hidden */
export function beforeOperation(
eventType: AuthBlockingEventType,
Expand Down
Loading