Skip to content

Commit

Permalink
Merge branch 'main' into feat/141
Browse files Browse the repository at this point in the history
  • Loading branch information
cka-y authored Nov 29, 2023
2 parents 20452af + 2bfaf3f commit 6f350df
Show file tree
Hide file tree
Showing 29 changed files with 8,916 additions and 90 deletions.
25 changes: 23 additions & 2 deletions .github/workflows/web-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
branches: [main]
paths:
- "web-app/**"
- "functions"
push:
branches: [main]
workflow_dispatch:
Expand Down Expand Up @@ -90,7 +91,7 @@ jobs:
build:
name: "Build & Deploy"
permissions: write-all
needs: [lint-test ]
needs: [lint-test]
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand Down Expand Up @@ -178,7 +179,27 @@ jobs:
working-directory: web-app
run: |
../scripts/replace-variables.sh -in_file src/.env.rename_me -out_file src/.env.${{ env.FIREBASE_PROJECT }} -variables REACT_APP_FIREBASE_API_KEY,REACT_APP_FIREBASE_AUTH_DOMAIN,REACT_APP_FIREBASE_PROJECT_ID,REACT_APP_FIREBASE_STORAGE_BUCKET,REACT_APP_FIREBASE_MESSAGING_SENDER_ID,REACT_REACT_APP_FIREBASE_APP_ID,REACT_APP_RECAPTCHA_SITE_KEY
- name: Run Install for Functions
working-directory: functions
run: yarn install

- name: Select Firebase Project for Functions
working-directory: functions
run: npx firebase use ${{ env.FIREBASE_PROJECT }}

- name: Run Lint for Functions
working-directory: functions
run: yarn lint

- name: Run Tests for Functions
working-directory: functions
run: yarn test

- name: Deploy Firebase Functions
working-directory: functions
run: npx firebase deploy --only functions

- name: Build
working-directory: web-app
run: yarn build:${FIREBASE_PROJECT}
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,13 @@ terraform.tfstate
terraform.tfstate.backup
secrets.env
secrets-*.env
vars.env
vars.envx
vars-*.env

config/.env.dev
venv

__pycache__
/functions-python/*/.env.local

**/node_modules
8 changes: 8 additions & 0 deletions functions/.firebaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"projects": {
"default": "mobility-feeds-dev",
"dev": "mobility-feeds-dev",
"qa": "mobility-feeds-qa",
"prod": "mobility-feeds-prod"
}
}
50 changes: 50 additions & 0 deletions functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Firebase Typescript Functions

## Overview
This project utilizes [Firebase Functions](https://firebase.google.com/docs/functions) within a monorepo architecture managed by [`yarn` workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/). It employs Firebase for deploying serverless functions.

## Project Structure
- `functions` directory: Contains the Firebase Functions.
- `packages` directory: Contains the function-specific code.
- `firebase.json`: Configuration file for Firebase services.
- `package.json`: Defines workspaces and scripts for managing the monorepo.

## Development
### Adding New Functions
1. **Create Function**: In the `packages/` directory, create a new directory for your function (e.g., `packages/my-new-function`).
2. **Setup Function**: Inside your function directory, initialize it with `yarn init` and set up your function code.
3. **Register Function**: Update `firebase.json` to include your new function. Add a new entry under `functions` with appropriate `source` and `codebase` values.
4. **Dependencies**: Manage any specific dependencies for your function within its directory.
### Local Development
1. **Build**: Run `yarn build` to build all workspaces.
2. **Emulate Functions**: Use `firebase emulators:start` to test functions locally.

## Deployment
### Build All Functions
Run `yarn build` to build all the functions.

### Deploy All Functions
To deploy the functions to Firebase use:
```shell
firebase deploy --only functions
```

### Deploy Functions Within a Codebase
To deploy a specific function to Firebase use:
```shell
firebase deploy --only functions:<codebase>
```

### Delete Functions
To delete a function from Firebase use:
```shell
firebase functions:delete <function_name>
```

## Testing
Run `yarn test` to execute tests across all workspaces.

## Linting
Run `yarn lint` to run linters across all workspaces.

---
24 changes: 24 additions & 0 deletions functions/firebase.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"functions": [
{
"source": "packages/user-api",
"codebase": "user-api",
"ignore": [
"node_modules",
".git",
"__tests__",
"firebase-debug.log",
"firebase-debug.*.log"
],
"predeploy": [
"yarn --cwd \"$RESOURCE_DIR\" install",
"yarn --cwd \"$RESOURCE_DIR\" build"
]
}
],
"emulators": {
"functions": {
"port": 5030
}
}
}
12 changes: 12 additions & 0 deletions functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"test": "yarn workspaces run test",
"build": "yarn workspaces run build",
"lint": "yarn workspaces run lint"
},
"devDependencies": {
"firebase-tools": "^12.5.4"
}
}
33 changes: 33 additions & 0 deletions functions/packages/user-api/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {
root: true,
env: {
es6: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",
"google",
"plugin:@typescript-eslint/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
project: ["tsconfig.json", "tsconfig.dev.json"],
sourceType: "module",
},
ignorePatterns: [
"/lib/**/*", // Ignore built files.
"**/*config.*", // Ignore config files.
],
plugins: [
"@typescript-eslint",
"import",
],
rules: {
"quotes": ["error", "double"],
"import/no-unresolved": 0,
"indent": ["error", 2],
},
};
17 changes: 17 additions & 0 deletions functions/packages/user-api/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# This file specifies files that are *not* uploaded to Google Cloud
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

node_modules
#!include:.gitignore
9 changes: 9 additions & 0 deletions functions/packages/user-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Compiled JavaScript files
lib/**/*.js
lib/**/*.js.map

# TypeScript v1 declaration files
typings/

# Node.js dependency directory
node_modules
5 changes: 5 additions & 0 deletions functions/packages/user-api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
};
39 changes: 39 additions & 0 deletions functions/packages/user-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "user-api",
"version": "1.0.0",
"scripts": {
"lint": "eslint --ext .js,.ts .",
"build": "tsc",
"build:watch": "tsc --watch",
"serve": "yarn build && firebase emulators:start --only functions",
"shell": "yarn build && firebase functions:shell",
"start": "yarn shell",
"test": "jest",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "18"
},
"main": "lib/index.js",
"dependencies": {
"@google-cloud/datastore": "^8.2.2",
"firebase": "^10.6.0",
"firebase-admin": "^11.8.0",
"firebase-functions": "^4.3.1"
},
"devDependencies": {
"@types/jest": "^29.5.8",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"eslint": "^8.9.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-import": "^2.25.4",
"firebase-functions-test": "^3.1.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.0"
},
"private": true
}
122 changes: 122 additions & 0 deletions functions/packages/user-api/src/__tests__/user-api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {
retrieveUser,
updateUserInformation,
retrieveUserInformation}
from "../impl/user-api-impl";
import {Datastore} from "@google-cloud/datastore";
import {CallableRequest, HttpsError} from "firebase-functions/v2/https";

jest.mock("@google-cloud/datastore");

describe("retrieveUser", () => {
it("should retrieve a user by uid", async () => {
const mockUid = "testUid";
const mockUser = {
uid: mockUid,
fullName: "Test User",
organization: "Test Org",
};

// mocking the Datastore query
Datastore.prototype.createQuery = jest.fn().mockReturnValue({
filter: jest.fn().mockReturnThis(),
limit: jest.fn().mockReturnThis(),
run: jest.fn().mockResolvedValue([[mockUser]]),
});
const mockDatastore = new Datastore();

const result = await retrieveUser(mockDatastore, mockUid);
expect(result).toEqual(mockUser);
});
});

describe("retrieveUserInformation", () => {
it("should throw an error if uid is not provided", async () => {
const mockRequest = {
auth: {uid: undefined},
rawRequest: {},
};
await expect(
retrieveUserInformation(mockRequest as unknown as CallableRequest))
.rejects
.toThrow(HttpsError);
});
});

describe("updateUserInformation", () => {
beforeEach(() => {
jest.mock("../impl/user-api-impl.ts", () => ({
retrieveUserKey: jest.fn().mockResolvedValue("userKey"),
}));
Datastore.prototype.save = jest.fn().mockReturnValue({
catch: jest.fn().mockReturnThis(),
});
});

it("should update user information", async () => {
const mockRequest = {
auth: {uid: "testUid"},
data: {fullName: "Test User", organization: "Test Org"},
rawRequest: {},
};
const result = await updateUserInformation(mockRequest as CallableRequest);
expect(result).toEqual("User testUid updated successfully.");
});

it("should update user information if organization is undefined",
async () => {
const mockRequest = {
auth: {uid: "testUid"},
data: {fullName: "Test User", organization: undefined},
rawRequest: {},
};
const result =
await updateUserInformation(mockRequest as CallableRequest);
expect(result).toEqual("User testUid updated successfully.");
});

it("should throw an error if uid is not provided", async () => {
const mockRequest = {
auth: {uid: undefined},
data: {fullName: "Test User", organization: "Test Org"},
rawRequest: {},
};
await expect(
updateUserInformation(mockRequest as unknown as CallableRequest))
.rejects
.toThrow(HttpsError);
});

it("should throw an error if fullName is not provided", async () => {
const mockRequest = {
auth: {uid: "testUid"},
data: {fullName: undefined, organization: "Test Org"},
rawRequest: {},
};
await expect(
updateUserInformation(mockRequest as unknown as CallableRequest))
.rejects
.toThrow(HttpsError);
});
it("should throw an HttpsError when save throws an error", async () => {
const mockRequest = {
auth: {uid: "testUid"},
data: {fullName: "Test User", organization: "Test Org"},
rawRequest: {},
};

Datastore.prototype.save = jest.fn().mockImplementation(() => {
throw new Error("Datastore save error");
});

await expect(
updateUserInformation(mockRequest as unknown as CallableRequest))
.rejects
.toThrow(HttpsError);

await expect(
updateUserInformation(mockRequest as unknown as CallableRequest))
.rejects
.toThrow(new HttpsError("internal", "Unable to update user information"));
});
});
Loading

0 comments on commit 6f350df

Please sign in to comment.