From 5188343671a6b631b83cd49edab6438ffa0d6944 Mon Sep 17 00:00:00 2001 From: shon-button Date: Mon, 17 Jul 2023 17:08:35 -0400 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=93=9A=20playwright=20config=20for?= =?UTF-8?q?=20auth=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 41 +- .env | 28 + .github/workflows/scan-code.yml | 9 +- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 2 +- README.md | 80 ++- app/[lng]/analyst/analytic/page.tsx | 6 +- app/[lng]/analyst/anonymized/[id]/page.tsx | 5 +- app/[lng]/analyst/anonymized/page.tsx | 4 +- app/[lng]/analyst/imported/[id]/page.tsx | 16 + app/[lng]/analyst/imported/page.tsx | 4 +- app/[lng]/auth/signin/page.tsx | 63 +- app/[lng]/error.tsx | 21 +- app/[lng]/unauth/page.tsx | 11 +- app/api/analyst/upload/route.ts | 2 +- app/api/auth/[...nextauth]/route.ts | 2 +- app/api/hello/route.ts | 8 + app/components/auth/SignIn.tsx | 69 +++ .../auth}/styles.module.css | 0 app/components/layout/Header.tsx | 2 +- app/components/query/DataTableQuery.tsx | 2 +- .../routes/anonymized/Anonymized.tsx | 42 +- app/components/routes/anonymized/id/Area.tsx | 6 +- app/components/routes/dataset/Add.tsx | 12 +- .../routes/dataset/connection/Query.tsx | 2 +- app/components/routes/imported/Imported.tsx | 43 +- app/components/routes/imported/id/Area.tsx | 5 +- app/i18n/client.ts | 2 +- app/i18n/locales/en/translation.json | 11 +- app/i18n/locales/fr/translation.json | 2 +- app/logs/errors/file.json | 34 ++ app/styles/globals.css | 2 +- app/types/declarations.d.ts | 8 +- app/types/next-auth.d.ts | 6 +- app/utils/{ => api}/auth/auth.ts | 38 +- app/utils/{ => api}/upload/post.ts | 7 +- app/utils/helpers.tsx | 82 --- app/utils/insight/tools.tsx | 2 +- app/utils/navigation/patties/manager.tsx | 10 +- app/utils/navigation/routes/manager.tsx | 14 +- app/utils/postgraphile/QueryRunner.js | 87 +++ app/utils/postgraphile/helpers.tsx | 84 +++ credentials/service-account-key-gcs.json | 13 + docker-compose.dev.yml | 1 - gitleaks.toml | 557 ------------------ middlewares/withAuthorization.tsx | 18 +- middlewares/withLocalization.tsx | 1 + next.config.js | 5 + package.json | 13 +- pages/api/analyst/graphql.ts | 54 +- pages/api/auth/role.ts | 20 + pages/api/{graphiql.ts => postgraphiql.ts} | 4 +- pages/api/{graphql.ts => postgraphql.ts} | 2 +- playwright.config.ts | 119 ++-- playwright/.auth/user.json | 25 + pnpm-lock.yaml | 352 ++++++++--- public/next.svg | 2 +- public/thirteen.svg | 2 +- public/vercel.svg | 2 +- tailwind.config.js | 2 +- tests-examples/demo-todo-app.spec.ts | 437 -------------- tests/.env | 3 + tests/auth.setup.ts | 70 +++ tests/gcs/file-upload.spec.ts | 183 ++++++ tests/gcs/files/helloWorld.csv | 6 + tests/gcs/files/helloWorld.json | 15 + tests/gcs/files/helloWorld.ods | Bin 0 -> 7963 bytes tests/gcs/files/helloWorld.xls | Bin 0 -> 5632 bytes tests/gcs/files/helloWorld.xlsx | Bin 0 -> 4767 bytes tests/gcs/files/helloWorld.xml | 37 ++ tests/i18n/lng-from-cookie.spec.ts | 4 +- tests/i18n/lng-from-default.spec.ts | 2 +- tests/i18n/lng-from-url-prefix.spec.ts | 27 +- tests/i18n/testUtils.ts | 6 +- 74 files changed, 1366 insertions(+), 1492 deletions(-) create mode 100644 .env create mode 100644 app/[lng]/analyst/imported/[id]/page.tsx create mode 100644 app/components/auth/SignIn.tsx rename app/{[lng]/auth/signin => components/auth}/styles.module.css (100%) rename app/utils/{ => api}/auth/auth.ts (63%) rename app/utils/{ => api}/upload/post.ts (97%) create mode 100644 app/utils/postgraphile/QueryRunner.js create mode 100644 app/utils/postgraphile/helpers.tsx create mode 100644 credentials/service-account-key-gcs.json delete mode 100644 gitleaks.toml create mode 100644 pages/api/auth/role.ts rename pages/api/{graphiql.ts => postgraphiql.ts} (87%) rename pages/api/{graphql.ts => postgraphql.ts} (94%) create mode 100644 playwright/.auth/user.json delete mode 100644 tests-examples/demo-todo-app.spec.ts create mode 100644 tests/.env create mode 100644 tests/auth.setup.ts create mode 100644 tests/gcs/file-upload.spec.ts create mode 100644 tests/gcs/files/helloWorld.csv create mode 100644 tests/gcs/files/helloWorld.json create mode 100644 tests/gcs/files/helloWorld.ods create mode 100644 tests/gcs/files/helloWorld.xls create mode 100644 tests/gcs/files/helloWorld.xlsx create mode 100644 tests/gcs/files/helloWorld.xml diff --git a/.dockerignore b/.dockerignore index e226817..310601f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,43 +1,58 @@ -/node_modules -.env -.env.local -Dockerfile -.git -.gitignore -docker-compose* - - -# files form git-ignore # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies + +/node_modules /.pnp .pnp.js # testing + /coverage # next.js + /.next/ /out/ # production + /build -/dist # misc + .DS_Store -*.pem +\*.pem # debug + npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* +# local env files + +.env*.local +.env.* + # vercel + .vercel # typescript -*.tsbuildinfo + +\*.tsbuildinfo next-env.d.ts +/test-results/ +/playwright-report/ +/playwright/.cache/ + +# keycloak + +keycloak-sa-credential.json + +# gcp + +/credentials/service-account-key.json +\nplaywright/.auth diff --git a/.env b/.env new file mode 100644 index 0000000..91f3c01 --- /dev/null +++ b/.env @@ -0,0 +1,28 @@ +API_HOST=http://localhost:3000/ +DATABASE=eed +DATABASE_HOST=34.125.92.26 +DATABASE_PORT=5432 +DATABASE_PROTOCOL=postgres +DATABASE_SCHEMA_ADMIN=eed +DATABASE_SCHEMA_CLEAN=data_clean_room +DATABASE_SCHEMA_WORKSPACE=data_science_workspace +DATABASE_SCHEMA_VAULT=published_vault +DATABASE_USER_ADMIN=eed_internal +DATABASE_USER_PW_ADMIN=secret +DATABASE_USER_ANALYST=eed_internal +DATABASE_USER_PW_ANALYST=secret +DATABASE_USER_DROPPER=eed_internal +DATABASE_USER_PW_DROPPER=secret +DATABASE_USER_MANAGER=eed_internal +DATABASE_USER_PW_MANAGER=secret +DATABASE_INSTANCE_CONNECTION_NAME=emissions-elt-demo:us-west4:eed +DB_USERNAME=keycloak_actor +DB_PASSWORD=keycloak_pass +GITHUB_ID=291382f53280c6de8f4d +GITHUB_SECRET=aaa22549c1066fbf1138505fa03a28e03ab045b8 +GOOGLE_BUCKET_NAME=eed_upload_file_storage +GOOGLE_CLIENT_ID=77682378143-061racvi899q8vvgmcioqhbnu8pokm9o.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +GOOGLE_PROJECT_NAME="emissions-elt-demo" +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk diff --git a/.github/workflows/scan-code.yml b/.github/workflows/scan-code.yml index 66c12cc..62ff715 100644 --- a/.github/workflows/scan-code.yml +++ b/.github/workflows/scan-code.yml @@ -9,12 +9,12 @@ concurrency: jobs: call-workflow-trivy-scan: uses: button-inc/gh-actions/.github/workflows/scan-code-trivy.yml@develop - + call-workflow-husky-scan: uses: button-inc/gh-actions/.github/workflows/scan-code-husky.yml@develop with: working-directory: ./app - node-version: '18' + node-version: '18' call-workflow-gitleaks-scan: uses: button-inc/gh-actions/.github/workflows/scan-code-gitleaks.yml@develop @@ -23,11 +23,8 @@ jobs: secrets: github-token: ${{ secrets.GITHUB_TOKEN }} gitleaks-license: ${{ secrets.GITLEAKS_LICENSE}} - + call-workflow-owasp-zap-scan: uses: button-inc/gh-actions/.github/workflows/scan-code-owasp-zap.yml@develop with: target-url: 'http://localhost:3000' - - - \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa19795..d08ab67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,4 +56,4 @@ repos: hooks: - id: commitlint stages: [commit-msg] - additional_dependencies: ["@commitlint/config-conventional"] \ No newline at end of file + additional_dependencies: ["@commitlint/config-conventional"] diff --git a/.vscode/settings.json b/.vscode/settings.json index ed2ca4d..61a5903 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "typescript.tsdk": "node_modules\\.pnpm\\typescript@4.9.4\\node_modules\\typescript\\lib", "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file +} diff --git a/README.md b/README.md index 5555f45..cc22b57 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,11 @@ See [NextAuth.js repo](https://github.com/nextauthjs/next-auth) to learn more. Within ClimateTrax, the next-auth functionality lies within folder `app\api\[...nextauth]\route.ts` and is managed within `middleware.ts` and `middlewares\withAuthorization.ts` -## `GOOGLE_APPLICATION_CREDENTIALS` +## postgraphile + +WIP + +## Authenticating with GCP The `GOOGLE_APPLICATION_CREDENTIALS` environment variable is used by various Google Cloud client libraries and command-line tools to authenticate and authorize access to Google Cloud services. It specifies the path to the service account key file, also known as the Application Default Credentials (ADC) file. @@ -208,12 +212,17 @@ To set the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable within Visu 1. Set the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: -Linux/Mac: +Linux/Mac: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json ``` -export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json +export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json ``` +**Note**: Storing a service account key file fro GCS within the file directory credentials/service-account-key-gcs.json, while not considered a best practice, has been done for convenience and ease of team collaboration, local GCP auth setup, and testing by using scripts to export the service account from the credential folder. +**Example**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server + +While this approach does provide convenience during local development and testing, it's important to handle sensitive credentials securely to protect against unauthorized access or exposure by using proper file permissions and preventing commit to GitHUb via the .gitignore file. + To echo the value of the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: Linux/Mac: @@ -248,6 +257,69 @@ On Linux or macOS: 5. Save the file. 6. Restart your terminal or run the `source` command to reload the environment variables in your current session. +## Testing + +### Playwright + +[Playwright](https://playwright.dev/) is a powerful browser automation library that allows you to control web browsers programmatically. +Playwright comes with the ability to generate tests out of the box and is a great way to quickly get started with testing. + +**Creating tests** + +You can run codegen and perform actions in the browser recorded as test scripts. Codegen will open two windows, a browser window where you interact with the website you wish to test and the Playwright Inspector window where you can record your tests, copy the tests, clear your tests as well as change the language of your tests. Playwright will generate the code for the user interactions. Codegen will look at the rendered page and figure out the recommended locator, prioritizing role, text and test id locators. If the generator identifies multiple elements matching the locator, it will improve the locator to make it resilient and uniquely identify the target element, therefore eliminating and reducing test(s) failing and flaking due to locators. + +Use the codegen command to run the test generator followed by the URL of the website you want to generate tests for. The URL is optional and you can always run the command without it and then add the URL directly into the browser window instead. + +``` +npx playwright codegen http://localhost:3000/ +``` + +You can also write tests manually following these suggested best practices: + +1. Use the right browser context: Playwright provides three browser options: `chromium`, `firefox`, and `webkit`. Choose the browser that best suits your needs in terms of features, performance, and compatibility. + +2. Close browser instances: Always close the browser instances and associated resources using the `close()` method. Failing to close the browser can lead to memory leaks and unexpected behavior. + +3. Reuse browser contexts: Reusing browser contexts can improve performance. Instead of creating a new context for each new page, consider creating a shared context and reusing it across multiple pages. + +4. Use the `waitFor` methods: Playwright offers `waitFor` methods (e.g., `waitForSelector`, `waitForNavigation`) that allow you to wait for specific conditions before proceeding with further actions. This helps ensure that the page has fully loaded or the desired element is available before interacting with it. + +5. Emulate network conditions: Playwright allows you to emulate various network conditions, such as slow connections or offline mode, using the `context.route` and `context.routeOverride` methods. This can be helpful for testing how your application behaves under different network scenarios. + +6. Handle errors and timeouts: Playwright operations can sometimes fail due to network issues, element unavailability, or other reasons. Properly handle errors and timeouts by using `try-catch` blocks and setting appropriate timeout values for operations like navigation or element waiting. + +7. Use `click` and `type` with caution: While using `click` and `type` methods, make sure to target the correct element and account for any potential delays caused by JavaScript events or animations on the page. + +8. Configure viewport and device emulation: Playwright allows you to set the viewport size and emulate different devices using the `page.setViewportSize` and `page.emulate` methods. Adjusting the viewport and device emulation can help test the responsiveness of your application. + +9. Use selective screenshotting: Capture screenshots strategically to minimize resource usage. Avoid taking excessive screenshots or capturing unnecessary parts of the page unless required for debugging or reporting. + +10. Run in headless mode: Consider running Playwright in headless mode (`headless: true`) for improved performance and resource utilization, especially in production or non-visual testing scenarios. + +11. Follow Playwright documentation: Playwright has comprehensive documentation with detailed guides, examples, and API references. Consult the official Playwright documentation (https://playwright.dev/) for specific use cases, best practices, and updates. + +**Running tests** + +Running tests can be completed using the package.json\scripts as follows: + +Testing end to end: + +``` +pnpm run test:e2e +``` + +Testing i18n: + +``` +pnpm run test:i18n +``` + +Testing GCS file upload: + +``` +pnpm run test:gcs +``` + ## Running App Locally ### run dev server @@ -359,5 +431,3 @@ pnpm run k8s ``` The output of `k8s` will be displayed in the terminal to confirm failure or success of setting a Kubernetes secret using shell "scripts\k8s-secrets.sh"; after which, Cloud Code\Run Kubernetes should launch - -gitleaks badge diff --git a/app/[lng]/analyst/analytic/page.tsx b/app/[lng]/analyst/analytic/page.tsx index 53ece13..2a62459 100644 --- a/app/[lng]/analyst/analytic/page.tsx +++ b/app/[lng]/analyst/analytic/page.tsx @@ -1,4 +1,8 @@ -import Analytic from "@/components/routes/Analytic"; +import dynamic from "next/dynamic"; +//๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server +const Analytic = dynamic(() => import("@/app/components/routes/Analytic"), { + ssr: false, +}); export default function Page() { return ( <> diff --git a/app/[lng]/analyst/anonymized/[id]/page.tsx b/app/[lng]/analyst/anonymized/[id]/page.tsx index d44da18..09757eb 100644 --- a/app/[lng]/analyst/anonymized/[id]/page.tsx +++ b/app/[lng]/analyst/anonymized/[id]/page.tsx @@ -7,13 +7,10 @@ export default function Page({ id: string; }; }) { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; - return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/analyst/anonymized/page.tsx b/app/[lng]/analyst/anonymized/page.tsx index fa77d3d..e0d0d45 100644 --- a/app/[lng]/analyst/anonymized/page.tsx +++ b/app/[lng]/analyst/anonymized/page.tsx @@ -1,12 +1,10 @@ import Anonymized from "@/components/routes/anonymized/Anonymized"; export default function Page() { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/analyst/imported/[id]/page.tsx b/app/[lng]/analyst/imported/[id]/page.tsx new file mode 100644 index 0000000..326aac0 --- /dev/null +++ b/app/[lng]/analyst/imported/[id]/page.tsx @@ -0,0 +1,16 @@ +import ImportedArea from "@/components/routes/imported/id/Area"; + +export default function Page({ + params, +}: { + params: { + id: string; + }; +}) { + return ( + <> + {/* @ts-expect-error Server Component */} + + + ); +} diff --git a/app/[lng]/analyst/imported/page.tsx b/app/[lng]/analyst/imported/page.tsx index 9bbe3a7..e4df39b 100644 --- a/app/[lng]/analyst/imported/page.tsx +++ b/app/[lng]/analyst/imported/page.tsx @@ -1,12 +1,10 @@ import Imported from "@/components/routes/imported/Imported"; export default function Page() { - // ๐Ÿ‘‡๏ธ graphQL query endpoint for this role - const endpoint = "api/analyst/graphql"; return ( <> {/* @ts-expect-error Server Component */} - + ); } diff --git a/app/[lng]/auth/signin/page.tsx b/app/[lng]/auth/signin/page.tsx index 72b56d6..4d716a3 100644 --- a/app/[lng]/auth/signin/page.tsx +++ b/app/[lng]/auth/signin/page.tsx @@ -1,71 +1,12 @@ -"use client"; -import { getProviders, signIn, ClientSafeProvider } from "next-auth/react"; -import React, { useEffect, useState } from "react"; -import styles from "./styles.module.css"; -import { useTranslation } from "@/i18n/client"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server -const Tag = dynamic(() => import("@/components/layout/Tag"), { +const SignIn = dynamic(() => import("@/components/auth/SignIn"), { ssr: false, }); export default function Page() { - const { t } = useTranslation("translation"); - const [data, setData] = useState | null>( - null - ); - - // ๐Ÿ‘‡๏ธ code running on the client-side should be placed inside a useEffect hook with the appropriate condition to ensure it only runs in the browser - useEffect(() => { - // ๐Ÿ‘‡๏ธ call to next-auth providers list - const fetchData = async () => { - const providers = await getProviders(); - setData(providers); - }; - - fetchData(); - }, []); - - // ๐Ÿ‘‡๏ธ render the providers as login buttons with the correct calback url - let hostUrl; - if (typeof window !== "undefined") { - hostUrl = window.location.origin; - } - // ๐Ÿ‘‡๏ธ nextauth signin calback url - const callbackUrl = - hostUrl && hostUrl.includes("http://localhost:4503") - ? "http://localhost:3000" - : process.env.NEXTAUTH_URL || "http://localhost:3000"; - - // ๐Ÿ‘‡๏ธ nextauth provider signin - const handleSignIn = async (providerId: string) => { - await signIn(providerId, { - callbackUrl, - }); - }; - - const content = data - ? Object.values(data).map((provider: ClientSafeProvider) => ( -
- -
- )) - : null; - return ( <> - - {content} + ); } diff --git a/app/[lng]/error.tsx b/app/[lng]/error.tsx index 438555f..d5905ca 100644 --- a/app/[lng]/error.tsx +++ b/app/[lng]/error.tsx @@ -1,11 +1,26 @@ "use client"; import { useTranslation } from "@/i18n/client"; +import React, { useEffect, useState } from "react"; +const NoSSR: React.FC = ({ children }) => { + const [isMounted, setIsMounted] = useState(false); + + useEffect(() => { + setIsMounted(true); + }, []); + + return isMounted ? <>{children} : null; +}; + export default async function Error() { // ๐Ÿ‘‡๏ธ language management const { t } = useTranslation("translation"); return ( - <> -

{t("messages.errors.error")}

- +
+ + {/* Code that should not be translated on the server side */}; +

{t("messages.errors.error")}

+
+ {/* The rest of page content */} +
); } diff --git a/app/[lng]/unauth/page.tsx b/app/[lng]/unauth/page.tsx index 7577527..d59b6ed 100644 --- a/app/[lng]/unauth/page.tsx +++ b/app/[lng]/unauth/page.tsx @@ -1,12 +1,11 @@ -"use client"; -import { useTranslation } from "@/i18n/client"; +import { useTranslation } from "@/i18n"; -export default function Page() { - // ๐Ÿ‘‡๏ธ client language management - const { t } = useTranslation("translation"); +export default async function Page() { + // ๐Ÿ‘‡๏ธ language management + const { i18n } = await useTranslation(); return ( <> -

โ›”๏ธ {t("messages.errors.unauth")}

+

{i18n.t("messages.errors.unauth")}

); } diff --git a/app/api/analyst/upload/route.ts b/app/api/analyst/upload/route.ts index bd0e2a5..397a88d 100644 --- a/app/api/analyst/upload/route.ts +++ b/app/api/analyst/upload/route.ts @@ -1,3 +1,3 @@ -import handler from "@/utils/upload/post"; +import handler from "@/app/utils/api/upload/post"; export { handler as POST }; diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index 5facd2a..572fb89 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,4 +1,4 @@ -import { authOptions } from "@/utils/auth/auth"; +import { authOptions } from "@/utils/api/auth/auth"; import NextAuth from "next-auth"; const handler = NextAuth(authOptions); diff --git a/app/api/hello/route.ts b/app/api/hello/route.ts index ca38e27..d2d4ed4 100644 --- a/app/api/hello/route.ts +++ b/app/api/hello/route.ts @@ -9,3 +9,11 @@ export async function GET() { return new Response("Hello, this is a new API route"); } +import { NextResponse } from "next/server"; + +export async function POST() { + const res = await fetch("https://data.mongodb-api.com/..."); + const data = await res.json(); + + return NextResponse.json({ data }); +} diff --git a/app/components/auth/SignIn.tsx b/app/components/auth/SignIn.tsx new file mode 100644 index 0000000..e112a69 --- /dev/null +++ b/app/components/auth/SignIn.tsx @@ -0,0 +1,69 @@ +"use client"; +import { getProviders, signIn, ClientSafeProvider } from "next-auth/react"; +import React, { useEffect, useState } from "react"; +import styles from "./styles.module.css"; +import { useTranslation } from "@/i18n/client"; +import Tag from "@/components/layout/Tag"; + +export default function Page() { + const { t } = useTranslation("translation"); + const [data, setData] = useState | null>( + null + ); + + // ๐Ÿ‘‡๏ธ code running on the client-side should be placed inside a useEffect hook with the appropriate condition to ensure it only runs in the browser + useEffect(() => { + // ๐Ÿ‘‡๏ธ call to next-auth providers list + const fetchData = async () => { + const providers = await getProviders(); + setData(providers); + }; + + fetchData(); + }, []); + + // ๐Ÿ‘‡๏ธ render the providers as login buttons with the correct calback url + let hostUrl; + if (typeof window !== "undefined") { + hostUrl = window.location.origin; + } + // ๐Ÿ‘‡๏ธ nextauth signin calback url + const callbackUrl = + hostUrl && hostUrl.includes("http://localhost:4503") + ? "http://localhost:3000" + : process.env.NEXTAUTH_URL || "http://localhost:3000"; + + // ๐Ÿ‘‡๏ธ nextauth provider signin + const handleSignIn = async (providerId: string) => { + await signIn(providerId, { + callbackUrl, + }); + }; + + const content = data + ? Object.values(data).map((provider: ClientSafeProvider) => ( +
+ +
+ )) + : null; + + return ( + <> + + {content} + + ); +} diff --git a/app/[lng]/auth/signin/styles.module.css b/app/components/auth/styles.module.css similarity index 100% rename from app/[lng]/auth/signin/styles.module.css rename to app/components/auth/styles.module.css diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index 48ed41e..5222ede 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -24,4 +24,4 @@ export default function Header() { ); -} \ No newline at end of file +} diff --git a/app/components/query/DataTableQuery.tsx b/app/components/query/DataTableQuery.tsx index 49b3a91..8c1a1ca 100644 --- a/app/components/query/DataTableQuery.tsx +++ b/app/components/query/DataTableQuery.tsx @@ -1,4 +1,4 @@ -import { getQueryData } from "@/utils/helpers"; +import { getQueryData } from "@/utils/postgraphile/helpers"; import { GraphqlQuery } from "@/types/declarations"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server diff --git a/app/components/routes/anonymized/Anonymized.tsx b/app/components/routes/anonymized/Anonymized.tsx index 06931f6..f974fee 100644 --- a/app/components/routes/anonymized/Anonymized.tsx +++ b/app/components/routes/anonymized/Anonymized.tsx @@ -1,41 +1,43 @@ +import { getSessionRoleEndpoint } from "@/utils/postgraphile/helpers"; import { Suspense } from "react"; import { gql } from "graphql-request"; import Spinner from "@/components/common/Spinner"; import DataTableQuery from "@/components/query/DataTableQuery"; import { columnsAnonymized } from "@/utils/table/columns"; import { crumbsAnonymized } from "@/utils/navigation/crumbs"; -import { GraphqlEndPoint } from "@/types/declarations"; import dynamic from "next/dynamic"; //๐Ÿ‘‡๏ธ will not be rendered on the server, prevents error: Text content did not match. Server const Tag = dynamic(() => import("@/components/layout/Tag"), { ssr: false, }); -// ๐Ÿ‘‡๏ธ graphQL query -const query = gql` - { - importRecords { - nodes { - jobId - fileName - submissionDate - trackFormat { - nickname - } - uploadedByUser { - email - } - } - } - } -`; // ๐Ÿ”ฅ workaround for error when trying to pass options as props with functions // โŒ "functions cannot be passed directly to client components because they're not serializable" // ๐Ÿ‘‡๏ธ used to changes options for @/components/table/DataTable const cntx = "anonymized"; -export default async function Page({ endpoint }: GraphqlEndPoint) { +export default async function Page() { + // ๐Ÿ‘‡๏ธ role base graphQL api route + const endpoint = await getSessionRoleEndpoint(); + // ๐Ÿ‘‡๏ธ graphQL query + const query = gql` + { + importRecords { + nodes { + jobId + fileName + submissionDate + trackFormat { + nickname + } + uploadedByUser { + email + } + } + } + } + `; // ๐Ÿ‘‰๏ธ RETURN: table with query data return ( <> diff --git a/app/components/routes/anonymized/id/Area.tsx b/app/components/routes/anonymized/id/Area.tsx index 71ad615..7d7b827 100644 --- a/app/components/routes/anonymized/id/Area.tsx +++ b/app/components/routes/anonymized/id/Area.tsx @@ -1,3 +1,4 @@ +import { getSessionRoleEndpoint } from "@/utils/postgraphile/helpers"; import { Suspense } from "react"; import { gql } from "graphql-request"; import Spinner from "@/components/common/Spinner"; @@ -11,7 +12,10 @@ import dynamic from "next/dynamic"; const Tag = dynamic(() => import("@/components/layout/Tag"), { ssr: false, }); -export default async function Page({ id, endpoint }: GraphqlParamEndPoint) { +export default async function Page({ id }: GraphqlParamEndPoint) { + // ๐Ÿ‘‡๏ธ role base graphQL api route + const endpoint = await getSessionRoleEndpoint(); + // ๐Ÿ‘‡๏ธ graphQL query const query = gql` diff --git a/app/components/routes/dataset/Add.tsx b/app/components/routes/dataset/Add.tsx index 144543c..2a85985 100644 --- a/app/components/routes/dataset/Add.tsx +++ b/app/components/routes/dataset/Add.tsx @@ -83,6 +83,8 @@ export default function Page({ endpoint }: GraphqlEndPoint) { setErrorMessage(errorMessage); } } + // Return the response or any relevant data + return response; } catch (error: any) { setErrorMessage(`An error occurred while uploading: ${error.message}`); } finally { @@ -99,7 +101,9 @@ export default function Page({ endpoint }: GraphqlEndPoint) { return ( <> {/* Mask overlay */} - {isMasked &&
} + {isMasked && ( +
+ )} {/* Tiles */}
@@ -114,7 +118,11 @@ export default function Page({ endpoint }: GraphqlEndPoint) {

{t("dataset.add.request")}

-
handleClickInputFile()}> +
handleClickInputFile()} + >
{t("dataset.add.file") import("@/components/layout/Tag"), { ssr: false, }); -// ๐Ÿ‘‡๏ธ graphQL query -const query = gql` - { - importRecords { - nodes { - jobId - fileName - submissionDate - trackFormat { - nickname - } - uploadedByUser { - email - } - } - } - } -`; // ๐Ÿ”ฅ workaround for error when trying to pass options as props with functions // โŒ "functions cannot be passed directly to client components because they're not serializable" // ๐Ÿ‘‡๏ธ used to change options for @/components/table/DataTable const cntx = "imported"; -export default async function Page({ endpoint }: GraphqlEndPoint) { +export default async function Page() { + // ๐Ÿ‘‡๏ธ role base graphQL api route + const endpoint = await getSessionRoleEndpoint(); + + // ๐Ÿ‘‡๏ธ graphQL query + const query = gql` + { + importRecords { + nodes { + jobId + fileName + submissionDate + trackFormat { + nickname + } + uploadedByUser { + email + } + } + } + } + `; // ๐Ÿ‘‰๏ธ RETURN: table with query data return ( <> diff --git a/app/components/routes/imported/id/Area.tsx b/app/components/routes/imported/id/Area.tsx index 709c7af..8be115f 100644 --- a/app/components/routes/imported/id/Area.tsx +++ b/app/components/routes/imported/id/Area.tsx @@ -1,3 +1,4 @@ +import { getSessionRoleEndpoint } from "@/utils/postgraphile/helpers"; import { Suspense } from "react"; import { gql } from "graphql-request"; import Spinner from "@/components/common/Spinner"; @@ -13,7 +14,9 @@ const Tag = dynamic(() => import("@/components/layout/Tag"), { }); // ๐Ÿ‘‡๏ธ used to changes options for @/components/table/DataTable const cntx = "dlpAnalysis"; -export default async function Page({ id, endpoint }: GraphqlParamEndPoint) { +export default async function Page({ id }: GraphqlParamEndPoint) { + const endpoint = await getSessionRoleEndpoint(); + // ๐Ÿ‘‡๏ธ graphQL query const query = gql` { diff --git a/app/i18n/client.ts b/app/i18n/client.ts index afcb0af..eda4722 100644 --- a/app/i18n/client.ts +++ b/app/i18n/client.ts @@ -1,6 +1,6 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; -import { useTranslation } from "next-i18next"; //import from next-i18next instead of react-i18next prevents error "NextJS+NextI18Next hydration error when trying to map through array: "Text content does not match server-rendered HTML". This is because the response of serverSideTranslations is a custom object with _nextI18Next property. +import { useTranslation } from "next-i18next"; //import from next-i18next instead of react-i18next prevents error "NextJS+NextI18Next hydration error when trying to map through array: "Text content does not match server-rendered HTML". This is because the response of serverSideTranslations is a custom object with _nextI18Next property. import resourcesToBackend from "i18next-resources-to-backend"; import LanguageDetector from "i18next-browser-languagedetector"; import { getOptions } from "./settings"; diff --git a/app/i18n/locales/en/translation.json b/app/i18n/locales/en/translation.json index e6130cf..8db1d9b 100644 --- a/app/i18n/locales/en/translation.json +++ b/app/i18n/locales/en/translation.json @@ -11,6 +11,9 @@ "3": "Imported By" } }, + "dataset": { + "tag": "Anonymized dataset" + }, "datasets": { "columns": { "0": "Dataset", @@ -66,6 +69,9 @@ "tag": "Home" }, "imported": { + "dataset": { + "tag": "Dataset" + }, "datasets": { "tag": "Anonymize a dataset" } @@ -192,6 +198,9 @@ }, "tag": "Imported dataset" }, + "dataset": { + "tag": "Dataset" + }, "datasets": { "columns": { "0": "Dataset", @@ -227,7 +236,7 @@ "messages": { "errors": { "error": "Ops, something went wrong!", - "label": "Attention:", + "label": "Error:", "notfound": "Page cannot be found.", "unauth": "Ops, you do not seem to have access to this area.", "upload": { diff --git a/app/i18n/locales/fr/translation.json b/app/i18n/locales/fr/translation.json index 6a929c6..6e58b0c 100644 --- a/app/i18n/locales/fr/translation.json +++ b/app/i18n/locales/fr/translation.json @@ -236,7 +236,7 @@ } }, "successes": { - "label": "FR Success:", + "label": "Succรจs:", "upload": "FR File uploaded." } } diff --git a/app/logs/errors/file.json b/app/logs/errors/file.json index 709dfac..b0d0588 100644 --- a/app/logs/errors/file.json +++ b/app/logs/errors/file.json @@ -12,3 +12,37 @@ {"timestamp":"2023-06-29T15:04:23.339Z","type":"upload","error":"Error: unsupported file type"} {"timestamp":"2023-06-29T15:11:36.700Z","type":"upload","error":"unsupported"} {"timestamp":"2023-06-29T15:13:01.317Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-05T17:14:14.966Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-05T17:20:05.277Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-17T19:33:17.347Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:33:48.083Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:34:18.816Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:34:55.469Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:35:26.255Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:35:56.784Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:40:12.523Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:40:43.196Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:41:13.711Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:41:45.857Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:42:16.689Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:42:47.191Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:47:04.994Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:47:35.627Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:48:06.154Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:48:38.454Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:53:32.643Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:54:03.402Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:54:33.892Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:55:06.121Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T19:55:36.923Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:01:38.259Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.csv&uploadType=resumable failed, reason: "} +{"timestamp":"2023-07-17T20:02:23.288Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:02:25.197Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:02:26.951Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:04:02.140Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:15:06.876Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:20:43.615Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.json&uploadType=resumable failed, reason: "} +{"timestamp":"2023-07-17T20:21:22.858Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:24:31.261Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-17T20:58:08.669Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.xlsx&uploadType=resumable failed, reason: "} +{"timestamp":"2023-07-17T20:58:41.189Z","type":"upload","error":"unsupported"} diff --git a/app/styles/globals.css b/app/styles/globals.css index 79b5905..c697251 100644 --- a/app/styles/globals.css +++ b/app/styles/globals.css @@ -17,4 +17,4 @@ a { } @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/app/types/declarations.d.ts b/app/types/declarations.d.ts index 0c81183..3e12f60 100644 --- a/app/types/declarations.d.ts +++ b/app/types/declarations.d.ts @@ -9,6 +9,9 @@ interface DataTableProps { columns: any[]; cntx?: string | null; } +interface GraphqlEndPoint { + endpoint: string; +} interface GraphqlQuery { endpoint: string; @@ -23,13 +26,8 @@ interface GraphqlResponse { }; } -interface GraphqlEndPoint { - endpoint: string; -} - interface GraphqlParamEndPoint { id: string; - endpoint: string; } interface TagProps { diff --git a/app/types/next-auth.d.ts b/app/types/next-auth.d.ts index d06fcfe..d4cf8ad 100644 --- a/app/types/next-auth.d.ts +++ b/app/types/next-auth.d.ts @@ -12,19 +12,19 @@ declare module "next-auth" { interface Session { user: { // ๐Ÿ‘‡๏ธ Module augmentation to add 'role' definition to the Session object - role?: string; + role?: string | any; } & DefaultSession["user"]; } } declare module "next-auth" { interface User { // ๐Ÿ‘‡๏ธ Module augmentation to add 'role' definition to the User object - role: string; + role?: string | any; } } declare module "next-auth/jwt" { // ๐Ÿ‘‡๏ธ Module augmentation to add 'role' definition to the JWT interface JWT { - role?: string; + role?: string | any; } } diff --git a/app/utils/auth/auth.ts b/app/utils/api/auth/auth.ts similarity index 63% rename from app/utils/auth/auth.ts rename to app/utils/api/auth/auth.ts index 334f0f9..5575bcf 100644 --- a/app/utils/auth/auth.ts +++ b/app/utils/api/auth/auth.ts @@ -1,11 +1,11 @@ import type { NextAuthOptions } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; +import GithubProvider from "next-auth/providers/github"; -//import { request, gql } from "graphql-request"; +import { request, gql } from "graphql-request"; async function getUserRole(email: string | null | undefined) { - /* - const endpoint = process.env.API_HOST + "api/role"; + const endpoint = process.env.API_HOST + "api/auth/role"; const query = gql` { @@ -13,15 +13,12 @@ async function getUserRole(email: string | null | undefined) { email + `" }) { nodes { - email userrole } } }`; const data: any = await request(endpoint, query); - return data[Object.keys(data)[0]].nodes as any[]; - */ - return "analyst"; // ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ + return data[Object.keys(data)[0]].nodes[0].userrole as any[]; } export const authOptions: NextAuthOptions = { @@ -36,17 +33,22 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GOOGLE_CLIENT_ID as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, }), + GithubProvider({ + clientId: process.env.GITHUB_ID as string, + clientSecret: process.env.GITHUB_SECRET as string, + }), ], callbacks: { async session({ session, token }) { - // ๐Ÿ‘‡๏ธ add role to the token from our permissions table if (!token.role) { + // ๐Ÿ‘‡๏ธ add role to the token from our permissions table const role = await getUserRole(token.email); if (role) { // ๐Ÿ‘‰๏ธ OK: set JWT role from our user record token.role = role; } } + return { ...session, user: { @@ -57,22 +59,14 @@ export const authOptions: NextAuthOptions = { }; }, // ๐Ÿ‘‡๏ธ called whenever a JSON Web Token is created - we can add to the JWT in this callback - async jwt({ token, user }) { - if (user) { - const u = user as unknown as any; + async jwt({ token }) { + if (!token.role) { // ๐Ÿ‘‡๏ธ add role to the token from our permissions table - if (!u.role) { - const role = await getUserRole(token.email); - if (role) { - // ๐Ÿ‘‰๏ธ OK: set JWT role from our user record - token.role = role; - } + const role = await getUserRole(token.email); + if (role) { + // ๐Ÿ‘‰๏ธ OK: set JWT role from our user record + token.role = role; } - return { - ...token, - id: u.id, - role: u.role, - }; } return token; }, diff --git a/app/utils/upload/post.ts b/app/utils/api/upload/post.ts similarity index 97% rename from app/utils/upload/post.ts rename to app/utils/api/upload/post.ts index d5af9d9..d9f882a 100644 --- a/app/utils/upload/post.ts +++ b/app/utils/api/upload/post.ts @@ -64,18 +64,19 @@ export default async function handler(request: NextRequest) { // ๐Ÿ‘‡๏ธ check file type const fileType = uploadedFile.type; let isValidFileType = false; - + console.log(fileType); switch (fileType) { case "application/json": case "application/vnd.ms-excel": case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": + case "application/xml": case "text/csv": case "text/xml": - // ๐Ÿ‘ + // ๐Ÿ‘ yes isValidFileType = true; break; default: - // ๐Ÿ‘Ž + // ๐Ÿ‘Ž no success = false; break; } diff --git a/app/utils/helpers.tsx b/app/utils/helpers.tsx index 79e0464..2f529fa 100644 --- a/app/utils/helpers.tsx +++ b/app/utils/helpers.tsx @@ -1,85 +1,3 @@ -import { cookies } from "next/headers"; -import { request, Variables } from "graphql-request"; -import { GraphqlResponse } from "@/types/declarations"; -import fs from "fs"; - -/** - * Flatten a nested JSON object - * @param object - The JSON object with nested JSON objects - */ -export const flattenJSON = ( - object: Record -): Record => { - let simpleObj: Record = {}; - for (let key in object) { - const value = object[key]; - const type = typeof value; - if ( - ["string", "boolean"].includes(type) || - (type === "number" && !isNaN(value)) - ) { - simpleObj[key] = value; - } else if (type === "object") { - // Recursive loop - Object.assign(simpleObj, flattenJSON(value)); - } - } - return simpleObj; -}; - -/** - * Perform a graphql-request to the API endpoint - * @param endpoint - The API route - * @param query - The postgraphile query - */ -export const getQueryData = async ( - endpoint: string, - query: string -): Promise => { - // Variables for graphql-request - endpoint = process.env.API_HOST + endpoint; - - const variables: Variables = {}; - // IMPORTANT: Add the browser session cookie with the encrypted JWT to this server-side API request - // To be used by middleware for route protection - const headers = { - Cookie: - "next-auth.session-token=" + - cookies().get("next-auth.session-token")?.value, - }; - // Data fetching via graphql-request - const response: GraphqlResponse = await request( - endpoint, - query, - variables, - headers - ); - // Get the nodes of the first object from the response - const nodes = response[Object.keys(response)[0]].nodes; - // Flatten nested nodes - const data = nodes.map((obj) => flattenJSON(obj)); - - // Return the data - return data; -}; - -/** - * Get a property by path utility - an alternative to lodash.get - * @param object - The object to traverse - * @param path - The path to the property - * @param defaultValue - The default value if the property is not found - */ -export const getPropByPath = ( - object: Record, - path: string | string[], - defaultValue?: any -): any => { - const myPath = Array.isArray(path) ? path : path.split("."); - if (object && myPath.length) - return getPropByPath(object[myPath.shift()!], myPath, defaultValue); - return object === undefined ? defaultValue : object; -}; - /** * Log message to stout or local json file * @param message - The message to log diff --git a/app/utils/insight/tools.tsx b/app/utils/insight/tools.tsx index 8d79b52..da4b6ca 100644 --- a/app/utils/insight/tools.tsx +++ b/app/utils/insight/tools.tsx @@ -25,4 +25,4 @@ export const biTools = [ text: "insight.tools.looker", tool: "looker" }, -]; \ No newline at end of file +]; diff --git a/app/utils/navigation/patties/manager.tsx b/app/utils/navigation/patties/manager.tsx index f1a8024..d5e61a7 100644 --- a/app/utils/navigation/patties/manager.tsx +++ b/app/utils/navigation/patties/manager.tsx @@ -13,15 +13,7 @@ export const menu: MenuItem[] = [ button: "home.routes.home.button", }, { - href: `${baseUrl}anonymized`, - button: "home.routes.anonymized.button", - }, - { - href: `${baseUrl}insight`, + href: `${baseUrl}test`, button: "home.routes.insight.button", }, - { - href: `${baseUrl}analytic`, - button: "home.routes.analytic.button", - }, ]; diff --git a/app/utils/navigation/routes/manager.tsx b/app/utils/navigation/routes/manager.tsx index 4299639..3db114a 100644 --- a/app/utils/navigation/routes/manager.tsx +++ b/app/utils/navigation/routes/manager.tsx @@ -2,22 +2,10 @@ import { RouteItem } from "@/types/declarations"; export const routes: RouteItem[] = [ - { - button: "home.routes.anonymized.button", - content: "home.routes.anonymized.content", - href: "anonymized", - title: "home.routes.anonymized.title", - }, { button: "home.routes.insight.button", content: "home.routes.insight.content", - href: "insight", + href: "test", title: "home.routes.insight.title", }, - { - button: "home.routes.analytic.button", - content: "home.routes.analytic.content", - href: "analytic", - title: "home.routes.analytic.title", - }, ]; diff --git a/app/utils/postgraphile/QueryRunner.js b/app/utils/postgraphile/QueryRunner.js new file mode 100644 index 0000000..92883cf --- /dev/null +++ b/app/utils/postgraphile/QueryRunner.js @@ -0,0 +1,87 @@ +const { Pool } = require("pg"); +const { graphql } = require("graphql"); +const { + withPostGraphileContext, + createPostGraphileSchema, +} = require("postgraphile"); + +async function makeQueryRunner( + connectionString, + schemaName, + options // See https://www.graphile.org/postgraphile/usage-schema/ for options +) { + // Create the PostGraphile schema + const schema = await createPostGraphileSchema( + connectionString, + schemaName, + options + ); + // Our database pool + const pgPool = new Pool({ + connectionString, + }); + + console.log( + "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$" + ); + console.log(connectionString); + console.log(schemaName); + console.log(options); + console.log(schema); + // The query function for issuing GraphQL queries + const query = async ( + graphqlQuery, // e.g. `{ __typename }` + variables = {}, + jwtToken = null, // A string, or null + operationName = null + ) => { + console.log( + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + ); + console.log(schema); + console.log(graphqlQuery); + // pgSettings and additionalContextFromRequest cannot be functions at this point + const pgSettings = options.pgSettings; + + console.log(graphqlQuery); + return await withPostGraphileContext( + { + ...options, + pgPool, + jwtToken: jwtToken, + pgSettings, + }, + async (context) => { + console.log( + "=============================================================================" + ); + console.log(schema); + console.log(graphqlQuery); + // Do NOT use context outside of this function. + return await graphql( + schema, + graphqlQuery, + null, + { + ...context, + /* You can add more to context if you like */ + }, + variables, + operationName + ); + } + ); + }; + + // Should we need to release this query runner, the cleanup tasks: + const release = () => { + pgPool.end(); + }; + + return { + query, + release, + }; +} + +exports.makeQueryRunner = makeQueryRunner; diff --git a/app/utils/postgraphile/helpers.tsx b/app/utils/postgraphile/helpers.tsx new file mode 100644 index 0000000..d327310 --- /dev/null +++ b/app/utils/postgraphile/helpers.tsx @@ -0,0 +1,84 @@ +import { cookies } from "next/headers"; +import { request, Variables } from "graphql-request"; +import { GraphqlResponse } from "@/types/declarations"; +import { authOptions } from "@/utils/api/auth/auth"; +import { getServerSession } from "next-auth/next"; +/** + * Flatten a nested JSON object + * @param object - The JSON object with nested JSON objects + */ +export const flattenJSON = ( + object: Record +): Record => { + try { + let simpleObj: Record = {}; + for (let key in object) { + const value = object[key]; + const type = typeof value; + if ( + ["string", "boolean"].includes(type) || + (type === "number" && !isNaN(value)) + ) { + simpleObj[key] = value; + } else if (type === "object") { + // Recursive loop + Object.assign(simpleObj, flattenJSON(value)); + } + } + return simpleObj; + } catch (error) { + console.error("An error occurred:", error); + throw error; // Re-throw the error to be caught by the calling code + } +}; + +/** + * Get server side session user role + */ + +export const getSessionRoleEndpoint = async (): Promise => { + const session = await getServerSession(authOptions); + const role = session?.user?.role ?? ""; + const endpoint = "api/" + role + "/graphql"; + return endpoint; +}; + +/** + * Perform a graphql-request to the API endpoint + * @param endpoint - The API route + * @param query - The postgraphile query + */ +export const getQueryData = async ( + endpoint: string, + query: string +): Promise => { + try { + // ๐Ÿ‘‡๏ธ variables for graphql-request + const variables: Variables = {}; + // โ— IMPORTANT - ๐Ÿช cookies: For serverside requests, add the browser session cookie with the encrypted JWT to this server-side API request to be used by middleware for route protection + const headers = { + Cookie: + "next-auth.session-token=" + + cookies().get("next-auth.session-token")?.value, + }; + // console.log(cookies().get("next-auth.session-token")?.value); + endpoint = process.env.API_HOST + endpoint; + // ๐Ÿ‘‡๏ธ data fetching via graphql-request + const response: GraphqlResponse = await request( + endpoint, + query, + variables, + headers + ); + + // ๐Ÿช“ mild hack.... + // ๐Ÿ‘‡๏ธ get the nodes of the first object from the response + const nodes = response[Object.keys(response)[0]].nodes; + // ๐Ÿ‘‡๏ธ flatten nested nodes + const data = nodes.map((obj) => flattenJSON(obj)); + return data; + } catch (error) { + console.error("An error occurred:", error); + throw error; + } +}; diff --git a/credentials/service-account-key-gcs.json b/credentials/service-account-key-gcs.json new file mode 100644 index 0000000..ad6a115 --- /dev/null +++ b/credentials/service-account-key-gcs.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "emissions-elt-demo", + "private_key_id": "ecc9c7e27bf42d989143745aea253a5f93e49aac", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT0/94jFq0fiwV\n9/FTyzc7SZ498sy3ZNJrXa0DRlnAP9YccbTBTCaa44So/xTrlEm5lP9XPd4CmKmD\nunRXGFo0L0wmyniO6HIeldclDMgzwOu4ptlreyL70Fc81hd2hre7Zu02xys2I4sw\nImieJ/tuWsRscYUgUeOLkIbPkG/UM42/XhYf9Kjx8cucQcui30WRwRxwbK3Mr/wr\nue9xyDgIFrE7ZBEiHA7OSt/Ae70FKqlAtMlWgR2LdXGts0Q1iA3Qy2kwPJMMPP5Y\n7KZJAq5wqRnx7WFNHqrAzkxcdYpVKVx+Q1loUJ9qM2lLhIKUS3mk7riO2PTOv0Fj\nfbz1fsXHAgMBAAECggEAMZSGWAuW9ndk2N9mUMjVFuzrhnJzJ8VIb5slBnanbnva\nl4KpcbVVM1jAqx+WiCadjYFEHKIS3oMOQ7CbCYUQ5/S/ETmSMrgSYmC2HmaJlRYM\n2UsYm9xaUOPBBpX1m5q2b8OnJtqpCwjjy3qW5Qia4xnNTGPMlxjv/OS12lLituQr\nFaaKnzo1cpGDSmtkIlyANV7NEfbe4b08fetPQOOUDMnQ4Cal78m8pURt5Ineps9a\nZ8sTPqU58fEe4oD7q/zbVF5DVltwq3xg/gjQAWwgOIve9E6TkCjjJH0Fli6miOi8\nWtHxu8MUpuHa6VpQGCvEs6jJtlPPMdr50E8N+37jFQKBgQDDMA4SBTQodYqQWi7x\nKQqkcnnALI2ZZVKUJlUzz+G0dL8d9zuYhYvDGUCFP2zd+TmID7RV3C4tJ4Rqt30q\n/Eg0WYCp+S5rCrmEl4q547Q54U3+zUR9IFgBsz2i8u7t0JX0B4QTCW9mQvlJA4pB\n+a1Hm+4/Khw6mn4NwylyakZrBQKBgQDB4pi3oRS8Cb34yRF6fhvycllAGy22z0lw\n88e6Tub8gq7dgiiw/+/ehPrBuqXczvK6gzRNO72oFHmz/UPSdW1xS58kauQ3W2tr\nbNdrqYOFkuO7ucQd8JGtEpI9MFZ8GT+Z672P7EFYKDqExyscXw7y0VqmcQkuiP52\n1IaCIIa/WwKBgQCLKQbPGECwm+T3uCSBsfYxeqCNP/aQqCmxEIdskkjkRNxBvBQU\nURptNeLHXYn71IWNGU1Ebd/KN8Nz5nBqJkZAdJOEG/FZReMwwm6Yy9yh652VDbpH\nz7iPNcC7HaL1kOJogrdKb06qRRPAV7LKCP3e8TByfk50BdPbcgpp1ZVxFQKBgEga\nMJj5enCDXvaKL8nR5CrBg5dnhBSb+b/bqMcMWLJHFyihIujQBTBHW8l30/7Np07d\nRDIEqX88PhZFdVdq/AxKByDP75b2lHgavfH31EV0XuSNLPXFZSdr5J6Ev2TfLtva\n42AGiDZ0n26JcurWHwUF/iQvnS6FG7ytRGhYGERJAoGAHxYwRdfjRCmdnbNvm1j3\n8KSkDWN6W2tEzgSJAQMf36G/ttbIdM93T/XY5xqfeC8cGZY732uFSkYkrRPboCA9\n9pMwctxacOr4uCsiFEV6PhFWu+hp9hMyHlgakrW4NVXb6GoU+DjruBX3ktLXBNe1\nJ+VrC317w6x8UdzvHHrEJ5g=\n-----END PRIVATE KEY-----\n", + "client_email": "cloud-storage-sa@emissions-elt-demo.iam.gserviceaccount.com", + "client_id": "106707473171516793046", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cloud-storage-sa%40emissions-elt-demo.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8bf7b1a..9b3fc9e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,4 +10,3 @@ services: - 3000:3000 volumes: - .:/usr/src/app - diff --git a/gitleaks.toml b/gitleaks.toml deleted file mode 100644 index c97ffd1..0000000 --- a/gitleaks.toml +++ /dev/null @@ -1,557 +0,0 @@ -title = "gitleaks config" - -# Gitleaks rules are defined by regular expressions and entropy ranges. -# Some secrets have unique signatures which make detecting those secrets easy. -# Examples of those secrets would be Gitlab Personal Access Tokens, AWS keys, and Github Access Tokens. -# All these examples have defined prefixes like `glpat`, `AKIA`, `ghp_`, etc. -# -# Other secrets might just be a hash which means we need to write more complex rules to verify -# that what we are matching is a secret. -# -# Here is an example of a semi-generic secret -# -# discord_client_secret = "8dyfuiRyq=vVc3RRr_edRk-fK__JItpZ" -# -# We can write a regular expression to capture the variable name (identifier), -# the assignment symbol (like '=' or ':='), and finally the actual secret. -# The structure of a rule to match this example secret is below: -# -# Beginning string -# quotation -# โ”‚ End string quotation -# โ”‚ โ”‚ -# โ–ผ โ–ผ -# (?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"] -# -# โ–ฒ โ–ฒ โ–ฒ -# โ”‚ โ”‚ โ”‚ -# โ”‚ โ”‚ โ”‚ -# identifier assignment symbol -# Secret -# - -[[rules]] -id = "gitlab-pat" -description = "GitLab Personal Access Token" -regex = '''glpat-[0-9a-zA-Z\-]{20}''' - -[[rules]] -id = "aws-access-token" -description = "AWS" -regex = '''(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}''' - -# Cryptographic keys -[[rules]] -id = "PKCS8-PK" -description = "PKCS8 private key" -regex = '''-----BEGIN PRIVATE KEY-----''' - -[[rules]] -id = "RSA-PK" -description = "RSA private key" -regex = '''-----BEGIN RSA PRIVATE KEY-----''' - -[[rules]] -id = "OPENSSH-PK" -description = "SSH private key" -regex = '''-----BEGIN OPENSSH PRIVATE KEY-----''' - -[[rules]] -id = "PGP-PK" -description = "PGP private key" -regex = '''-----BEGIN PGP PRIVATE KEY BLOCK-----''' - -[[rules]] -id = "github-pat" -description = "Github Personal Access Token" -regex = '''ghp_[0-9a-zA-Z]{36}''' - -[[rules]] -id = "github-oauth" -description = "Github OAuth Access Token" -regex = '''gho_[0-9a-zA-Z]{36}''' - -[[rules]] -id = "SSH-DSA-PK" -description = "SSH (DSA) private key" -regex = '''-----BEGIN DSA PRIVATE KEY-----''' - -[[rules]] -id = "SSH-EC-PK" -description = "SSH (EC) private key" -regex = '''-----BEGIN EC PRIVATE KEY-----''' - - -[[rules]] -id = "github-app-token" -description = "Github App Token" -regex = '''(ghu|ghs)_[0-9a-zA-Z]{36}''' - -[[rules]] -id = "github-refresh-token" -description = "Github Refresh Token" -regex = '''ghr_[0-9a-zA-Z]{76}''' - -[[rules]] -id = "shopify-shared-secret" -description = "Shopify shared secret" -regex = '''shpss_[a-fA-F0-9]{32}''' - -[[rules]] -id = "shopify-access-token" -description = "Shopify access token" -regex = '''shpat_[a-fA-F0-9]{32}''' - -[[rules]] -id = "shopify-custom-access-token" -description = "Shopify custom app access token" -regex = '''shpca_[a-fA-F0-9]{32}''' - -[[rules]] -id = "shopify-private-app-access-token" -description = "Shopify private app access token" -regex = '''shppa_[a-fA-F0-9]{32}''' - -[[rules]] -id = "slack-access-token" -description = "Slack token" -regex = '''xox[baprs]-([0-9a-zA-Z]{10,48})?''' - -[[rules]] -id = "stripe-access-token" -description = "Stripe" -regex = '''(?is)(sk|pk)_(test|live)_[0-9a-z]{10,32}''' - -[[rules]] -id = "pypi-upload-token" -description = "PyPI upload token" -regex = '''pypi-AgEIcHlwaS5vcmc[A-Za-z0-9-_]{50,1000}''' - -[[rules]] -id = "gcp-service-account" -description = "Google (GCP) Service-account" -regex = '''\"type\": \"service_account\"''' - -[[rules]] -id = "heroku-api-key" -description = "Heroku API Key" -regex = ''' (?is)(heroku[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})['\"]''' -secretGroup = 3 - -[[rules]] -id = "slack-web-hook" -description = "Slack Webhook" -regex = '''https://hooks.slack.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8,12}/[a-zA-Z0-9_]{24}''' - -[[rules]] -id = "twilio-api-key" -description = "Twilio API Key" -regex = '''SK[0-9a-fA-F]{32}''' - -[[rules]] -id = "age-secret-key" -description = "Age secret key" -regex = '''AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}''' - -[[rules]] -id = "facebook-token" -description = "Facebook token" -regex = '''(?is)(facebook[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "twitter-token" -description = "Twitter token" -regex = '''(?is)(twitter[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{35,44})['\"]''' -secretGroup = 3 - -[[rules]] -id = "adobe-client-id" -description = "Adobe Client ID (Oauth Web)" -regex = '''(?is)(adobe[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "adobe-client-secret" -description = "Adobe Client Secret" -regex = '''(p8e-)(?is)[a-z0-9]{32}''' - -[[rules]] -id = "alibaba-access-key-id" -description = "Alibaba AccessKey ID" -regex = '''(LTAI)(?is)[a-z0-9]{20}''' - -[[rules]] -id = "alibaba-secret-key" -description = "Alibaba Secret Key" -regex = '''(?is)(alibaba[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]''' -secretGroup = 3 - -[[rules]] -id = "asana-client-id" -description = "Asana Client ID" -regex = '''(?is)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{16})['\"]''' -secretGroup = 3 - -[[rules]] -id = "asana-client-secret" -description = "Asana Client Secret" -regex = '''(?is)(asana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "atlassian-api-token" -description = "Atlassian API token" -regex = '''(?is)(atlassian[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{24})['\"]''' -secretGroup = 3 - -[[rules]] -id = "bitbucket-client-id" -description = "Bitbucket client ID" -regex = '''(?is)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "bitbucket-client-secret" -description = "Bitbucket client secret" -regex = '''(?is)(bitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9_\-]{64})['\"]''' -secretGroup = 3 - -[[rules]] -id = "beamer-api-token" -description = "Beamer API token" -regex = '''(?is)(beamer[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](b_[a-z0-9=_\-]{44})['\"]''' -secretGroup = 3 - -[[rules]] -id = "clojars-api-token" -description = "Clojars API token" -regex = '''(CLOJARS_)(?is)[a-z0-9]{60}''' - -[[rules]] -id = "contentful-delivery-api-token" -description = "Contentful delivery API token" -regex = '''(?is)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]''' -secretGroup = 3 - -[[rules]] -id = "contentful-preview-api-token" -description = "Contentful preview API token" -regex = '''(?is)(contentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{43})['\"]''' -secretGroup = 3 - -[[rules]] -id = "databricks-api-token" -description = "Databricks API token" -regex = '''dapi[a-h0-9]{32}''' - -[[rules]] -id = "discord-api-token" -description = "Discord API key" -regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{64})['\"]''' -secretGroup = 3 - -[[rules]] -id = "discord-client-id" -description = "Discord client ID" -regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([0-9]{18})['\"]''' -secretGroup = 3 - -[[rules]] -id = "discord-client-secret" -description = "Discord client secret" -regex = '''(?is)(discord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_\-]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "doppler-api-token" -description = "Doppler API token" -regex = '''['\"](dp\.pt\.)(?is)[a-z0-9]{43}['\"]''' - -[[rules]] -id = "dropbox-api-secret" -description = "Dropbox API secret/key" -regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]''' - -[[rules]] -id = "dropbox--api-key" -description = "Dropbox API secret/key" -regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]''' - -[[rules]] -id = "dropbox-short-lived-api-token" -description = "Dropbox short lived API token" -regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](sl\.[a-z0-9\-=_]{135})['\"]''' - -[[rules]] -id = "dropbox-long-lived-api-token" -description = "Dropbox long lived API token" -regex = '''(?is)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"][a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}['\"]''' - -[[rules]] -id = "duffel-api-token" -description = "Duffel API token" -regex = '''['\"]duffel_(test|live)_(?is)[a-z0-9_-]{43}['\"]''' - -[[rules]] -id = "dynatrace-api-token" -description = "Dynatrace API token" -regex = '''['\"]dt0c01\.(?is)[a-z0-9]{24}\.[a-z0-9]{64}['\"]''' - -[[rules]] -id = "easypost-api-token" -description = "EasyPost API token" -regex = '''['\"]EZAK(?is)[a-z0-9]{54}['\"]''' - -[[rules]] -id = "easypost-test-api-token" -description = "EasyPost test API token" -regex = '''['\"]EZTK(?is)[a-z0-9]{54}['\"]''' - -[[rules]] -id = "fastly-api-token" -description = "Fastly API token" -regex = '''(?is)(fastly[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9\-=_]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "finicity-client-secret" -description = "Finicity client secret" -regex = '''(?is)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{20})['\"]''' -secretGroup = 3 - -[[rules]] -id = "finicity-api-token" -description = "Finicity API token" -regex = '''(?is)(finicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "flutterweave-public-key" -description = "Flutterweave public key" -regex = '''FLWPUBK_TEST-(?is)[a-h0-9]{32}-X''' - -[[rules]] -id = "flutterweave-secret-key" -description = "Flutterweave secret key" -regex = '''FLWSECK_TEST-(?is)[a-h0-9]{32}-X''' - -[[rules]] -id = "flutterweave-enc-key" -description = "Flutterweave encrypted key" -regex = '''FLWSECK_TEST[a-h0-9]{12}''' - -[[rules]] -id = "frameio-api-token" -description = "Frame.io API token" -regex = '''fio-u-(?is)[a-z0-9-_=]{64}''' - -[[rules]] -id = "gocardless-api-token" -description = "GoCardless API token" -regex = '''['\"]live_(?is)[a-z0-9-_=]{40}['\"]''' - -[[rules]] -id = "grafana-api-token" -description = "Grafana API token" -regex = '''['\"]eyJrIjoi(?is)[a-z0-9-_=]{72,92}['\"]''' - -[[rules]] -id = "hashicorp-tf-api-token" -description = "Hashicorp Terraform user/org API token" -regex = '''['\"](?is)[a-z0-9]{14}\.atlasv1\.[a-z0-9-_=]{60,70}['\"]''' - -[[rules]] -id = "hubspot-api-token" -description = "Hubspot API token" -regex = '''(?is)(hubspot[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' -secretGroup = 3 - -[[rules]] -id = "intercom-api-token" -description = "Intercom API token" -regex = '''(?is)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9=_]{60})['\"]''' -secretGroup = 3 - -[[rules]] -id = "intercom-client-secret" -description = "Intercom client secret/ID" -regex = '''(?is)(intercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' -secretGroup = 3 - -[[rules]] -id = "ionic-api-token" -description = "Ionic API token" -regex = '''(?is)(ionic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](ion_[a-z0-9]{42})['\"]''' - -[[rules]] -id = "linear-api-token" -description = "Linear API token" -regex = '''lin_api_(?is)[a-z0-9]{40}''' - -[[rules]] -id = "linear-client-secret" -description = "Linear client secret/ID" -regex = '''(?is)(linear[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "lob-api-key" -description = "Lob API Key" -regex = '''(?is)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((live|test)_[a-f0-9]{35})['\"]''' -secretGroup = 3 - -[[rules]] -id = "lob-pub-api-key" -description = "Lob Publishable API Key" -regex = '''(?is)(lob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]((test|live)_pub_[a-f0-9]{31})['\"]''' -secretGroup = 3 - -[[rules]] -id = "mailchimp-api-key" -description = "Mailchimp API key" -regex = '''(?is)(mailchimp[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-f0-9]{32}-us20)['\"]''' -secretGroup = 3 - -[[rules]] -id = "mailgun-private-api-token" -description = "Mailgun private API token" -regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](key-[a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "mailgun-pub-key" -description = "Mailgun public validation key" -regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](pubkey-[a-f0-9]{32})['\"]''' -secretGroup = 3 - -[[rules]] -id = "mailgun-signing-key" -description = "Mailgun webhook signing key" -regex = '''(?is)(mailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\"]''' -secretGroup = 3 - -[[rules]] -id = "mapbox-api-token" -description = "Mapbox API token" -regex = '''(?is)(pk\.[a-z0-9]{60}\.[a-z0-9]{22})''' - -[[rules]] -id = "messagebird-api-token" -description = "MessageBird API token" -regex = '''(?is)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{25})['\"]''' -secretGroup = 3 - -[[rules]] -id = "messagebird-client-id" -description = "MessageBird API client ID" -regex = '''(?is)(messagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]''' -secretGroup = 3 - -[[rules]] -id = "new-relic-user-api-key" -description = "New Relic user API Key" -regex = '''['\"](NRAK-[A-Z0-9]{27})['\"]''' - -[[rules]] -id = "new-relic-user-api-id" -description = "New Relic user API ID" -regex = '''(?is)(newrelic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([A-Z0-9]{64})['\"]''' -secretGroup = 3 - -[[rules]] -id = "new-relic-browser-api-token" -description = "New Relic ingest browser API token" -regex = '''['\"](NRJS-[a-f0-9]{19})['\"]''' - -[[rules]] -id = "npm-access-token" -description = "npm access token" -regex = '''['\"](npm_(?is)[a-z0-9]{36})['\"]''' - -[[rules]] -id = "planetscale-password" -description = "Planetscale password" -regex = '''pscale_pw_(?is)[a-z0-9\-_\.]{43}''' - -[[rules]] -id = "planetscale-api-token" -description = "Planetscale API token" -regex = '''pscale_tkn_(?is)[a-z0-9\-_\.]{43}''' - -[[rules]] -id = "postman-api-token" -description = "Postman API token" -regex = '''PMAK-(?is)[a-f0-9]{24}\-[a-f0-9]{34}''' - -[[rules]] -id = "pulumi-api-token" -description = "Pulumi API token" -regex = '''pul-[a-f0-9]{40}''' - -[[rules]] -id = "rubygems-api-token" -description = "Rubygem API token" -regex = '''rubygems_[a-f0-9]{48}''' - -[[rules]] -id = "sendgrid-api-token" -description = "Sendgrid API token" -regex = '''SG\.(?is)[a-z0-9_\-\.]{66}''' - -[[rules]] -id = "sendinblue-api-token" -description = "Sendinblue API token" -regex = '''xkeysib-[a-f0-9]{64}\-(?is)[a-z0-9]{16}''' - -[[rules]] -id = "shippo-api-token" -description = "Shippo API token" -regex = '''shippo_(live|test)_[a-f0-9]{40}''' - -[[rules]] -id = "linedin-client-secret" -description = "Linkedin Client secret" -regex = '''(?is)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z]{16})['\"]''' -secretGroup = 3 - -[[rules]] -id = "linedin-client-id" -description = "Linkedin Client ID" -regex = '''(?is)(linkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{14})['\"]''' -secretGroup = 3 - -[[rules]] -id = "twitch-api-token" -description = "Twitch API token" -regex = '''(?is)(twitch[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{30})['\"]''' -secretGroup = 3 - -[[rules]] -id = "typeform-api-token" -description = "Typeform API token" -regex = '''(?is)(typeform[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}(tfp_[a-z0-9\-_\.=]{59})''' -secretGroup = 3 - -# A generic rule to match things that look like a secret -# (?is): i means case insensitive, m means multiline. -# We allow for unlimited whitespace character on each side of the assignment operator -[[rules]] -id = "generic-api-key" -description = "Generic API Key" -regex = '''(?is)((key|api|token|secret|password)[a-z0-9_ .\-,]{0,50})\s*(=|>|:=|\|\|:|<=|=>|:)\s*.{0,5}['\"]([0-9a-zA-Z\-_=]{8,192})['\"]''' -entropy = 3.7 -secretGroup = 4 - -[[rules]] -description = "Database password" -regex = '''databasePW\s*=\s*".*?"''' -tags = ["database", "password"] - -[allowlist] -description = "global allow lists" -regexes = ['''(example_regex)'''] -paths = [ - '''.gitleaks.toml''', - '''(.*?)(jpg|gif|doc|pdf|bin|svg|socket)$''' -] diff --git a/middlewares/withAuthorization.tsx b/middlewares/withAuthorization.tsx index b358547..1c3b969 100644 --- a/middlewares/withAuthorization.tsx +++ b/middlewares/withAuthorization.tsx @@ -19,9 +19,19 @@ export const withAuthorization: MiddlewareFactory = (next: NextMiddleware) => { // ๐Ÿ‘‡๏ธ vars for route management const { pathname } = request.nextUrl; + const isRouteGraphQL = pathname.indexOf("api/postgraph") > -1; const isRouteAuth = pathname.indexOf("/auth") > -1 || pathname.indexOf("/unauth") > -1; + // ๐Ÿ‘‡๏ธ check calls to graphql + if (isRouteGraphQL === true) { + // ๐Ÿ‘€ dev only + if (process.env.API_HOST == "http://localhost:3000/") { + // ๐Ÿ‘‰๏ธ OK: route all postgraphile routes + return NextResponse.next(); + } + } + // ๐Ÿ‘‡๏ธ check if authentication route if (isRouteAuth === true) { const { query } = parse(request.url, true); @@ -35,14 +45,14 @@ export const withAuthorization: MiddlewareFactory = (next: NextMiddleware) => { // ๐Ÿ‘‰๏ธ return response NextResponse.next(); } else { - // ๐Ÿ‘‡๏ธ vars for user session details via next-auth getToken to decrypt jwt in request cookie - const session = await getToken({ + // ๐Ÿ‘‡๏ธ vars for user token details via next-auth getToken to decrypt jwt in request cookie + const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET, }); - const role = session?.role ? session?.role : "analyst"; // ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ ๐Ÿšจ; + const role = token?.role; - if (session && role) { + if (token && role) { // ๐Ÿ‘‰๏ธ OK: authenticated and authorized role // ๐Ÿ‘‡๏ธ validate routes properties diff --git a/middlewares/withLocalization.tsx b/middlewares/withLocalization.tsx index 13bb72b..696c466 100644 --- a/middlewares/withLocalization.tsx +++ b/middlewares/withLocalization.tsx @@ -11,6 +11,7 @@ export const withLocalization: MiddlewareFactory = (next) => { return async (request: NextRequest, _next: NextFetchEvent) => { // 1๏ธโƒฃ Check (valid) Language Prefix in URL const { pathname } = request.nextUrl; + //๐Ÿ‘‡๏ธ the first non-empty segment is considered the language prefix const [languagePrefix] = pathname.split("/").filter(Boolean); // ๐Ÿ‘‡๏ธ validate the language is supported from the accepted languages diff --git a/next.config.js b/next.config.js index 6a3b0d9..a40865a 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,12 @@ /** @type {import("next").NextConfig} */ +require("dotenv").config(); + const nextConfig = { experimental: { appDir: true, }, + env: { + API_HOST: process.env.API_HOST, + }, }; module.exports = nextConfig; diff --git a/package.json b/package.json index 4e0dbe4..fa41f0a 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,14 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && next dev", "build": "next build", "start": "next start", "lint": "next lint", + "test:codegen": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && npx playwright codegen", + "test:e2e": "playwright test", + "test:i18n": "playwright test tests/i18n", + "test:gcs": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && playwright test tests/gcs", "k8s": "minikube start && scripts/k8s-secrets.sh && scripts/k8s-launch.sh", "tailwind:watch": "postcss tailwind.css -o styles.css -w" }, @@ -26,6 +30,8 @@ "@types/react-dom": "18.2.4", "accept-language": "3.0.18", "autoprefixer": "10.4.14", + "bufferutil": "^4.0.7", + "encoding": "^0.1.13", "eslint": "8.40.0", "eslint-config-next": "13.4.2", "eslint-config-prettier": "^8.8.0", @@ -33,10 +39,12 @@ "eslint-plugin-react": "^7.32.2", "fetch-blob": "^4.0.0", "formdata-polyfill": "^4.0.10", + "graphql": "^16.7.1", "graphql-request": "^5.2.0", "i18next": "^22.5.1", "i18next-browser-languagedetector": "7.0.1", "i18next-resources-to-backend": "1.1.3", + "mock-express-request": "^0.2.2", "mui-datatables": "^4.3.0", "next": "latest", "next-auth": "^4.22.1", @@ -52,7 +60,8 @@ "react-i18next": "12.1.1", "stream": "^0.0.2", "tailwindcss": "3.3.2", - "typescript": "5.0.4" + "typescript": "5.0.4", + "utf-8-validate": "^6.0.3" }, "devDependencies": { "@playwright/test": "^1.35.0", diff --git a/pages/api/analyst/graphql.ts b/pages/api/analyst/graphql.ts index 58936f4..ed0d092 100644 --- a/pages/api/analyst/graphql.ts +++ b/pages/api/analyst/graphql.ts @@ -1,3 +1,37 @@ +/* +Next.js pages\API route handler method receives two arguments: req and res. + +The req argument represents the incoming HTTP request to the API route. +It contains information about the request, such as headers, query parameters, request body, and more. +It is an instance of the Node.js http.IncomingMessage class. + +The res argument represents the HTTP response object that you use to send a response back to the client. + It provides methods for setting response headers, writing the response body, and managing the response status code. + It is an instance of the Node.js http.ServerResponse class. + +The req object is an instance of http.IncomingMessage, and it represents the request information received from the client sending the request, +it also includes the request body, query, headers, method, URL, etc. +Next.js also provides built-in middlewares for parsing and extending the incoming request (req) object. These are the middlewares: + +req.body: By default, Next.js parses the request body, so you donโ€™t have to install another third-party body-parser module. req.body comprises an object parsed by content-type as specified by the client. It defaults to null if no body was specified. + +req.query: Next.js also parses the query string attached to the request URL. req.query is an object containing the query parameters and their values, or an empty object {} if no query string was attached. + +req.cookies: It contains the cookies sent by the request. It also defaults to an empty object {} if no cookies are specified. + +The response (res) object. It is an instance of http.ServerResponse, with additional helper methods + +res.json(body): It is used to send a JSON response back to the client. It takes as an argument an object which must be serializable. + +res.send(body): Used to send the HTTP response. The body can either be a string, an object or a buffer. + +res.status(code): It is a function used to set the response status code. It accepts a valid HTTP status code as an argument. + +Next.js provides a config object that, when exported, can be used to change some of the default configurations of the application. It has a nested api object that deals with configurations available for API routes. + +For example, we can disable the default body parser provided by Next.js. +*/ + import { postgraphile } from "postgraphile"; import { pgAnalyst } from "@/utils/postgraphile/pool/pgAnalyst"; import { options } from "@/utils/postgraphile/options"; @@ -6,19 +40,27 @@ const databaseSchemaAdmin = process.env.DATABASE_SCHEMA_ADMIN || ""; const databaseSchemaClean = process.env.DATABASE_SCHEMA_CLEAN || ""; const databaseSchemaWorkspace = process.env.DATABASE_SCHEMA_WORKSPACE || ""; +// ๐Ÿ‘‡๏ธ customize the default configuration of the API route by exporting a config object in the same file +export const config = { + api: { + bodyParser: false, // Defaults to true. Setting this to false disables body parsing and allows you to consume the request body as stream or raw-body. + responseLimit: false, // Determines how much data should be sent from the response body. It is automatically enabled and defaults to 4mb. + externalResolver: true, // Disables warnings for unresolved requests if the route is being handled by an external resolver + }, +}; + +// ๐Ÿ‘‡๏ธ postgraphile function returns an object assigned to the requestHandler variable const requestHandler = postgraphile( pgAnalyst, [databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace], { ...options, + // ๐Ÿ‘‡๏ธ specifies the role based route where this GraphQL API will be accessible graphqlRoute: "/api/analyst/graphql", } ); -export const config = { - api: { - bodyParser: false, - externalResolver: true, - }, -}; +/*When a request is made to the specified Next.js route, the requestHandler executes the logic provided by PostGraphile. +It connects to the PostgreSQL database using the provided connection pool (pgAnalyst) and exposes a GraphQL API based on the specified schemas and options. +It handles the execution of GraphQL queries and returns the corresponding data as per the GraphQL request.*/ export default requestHandler; diff --git a/pages/api/auth/role.ts b/pages/api/auth/role.ts new file mode 100644 index 0000000..9d69ff2 --- /dev/null +++ b/pages/api/auth/role.ts @@ -0,0 +1,20 @@ +import { postgraphile } from "postgraphile"; +import { pgAdmin } from "@/utils/postgraphile/pool/pgAdmin"; +import { options } from "@/utils/postgraphile/options"; + +const requestHandler = postgraphile( + pgAdmin, + process.env.DATABASE_SCHEMA_ADMIN, + { + ...options, + graphqlRoute: "/api/auth/role", + } +); + +export const config = { + api: { + bodyParser: false, + externalResolver: true, + }, +}; +export default requestHandler; diff --git a/pages/api/graphiql.ts b/pages/api/postgraphiql.ts similarity index 87% rename from pages/api/graphiql.ts rename to pages/api/postgraphiql.ts index dfb2b29..4deb773 100644 --- a/pages/api/graphiql.ts +++ b/pages/api/postgraphiql.ts @@ -11,8 +11,8 @@ const requestHandler = postgraphile( [databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace], { ...options, - graphiqlRoute: "/api/graphiql", - graphqlRoute: "/api/graphql", + graphiqlRoute: "/api/postgraphiql", + graphqlRoute: "/api/postgraphql", } ); diff --git a/pages/api/graphql.ts b/pages/api/postgraphql.ts similarity index 94% rename from pages/api/graphql.ts rename to pages/api/postgraphql.ts index de48789..97604cb 100644 --- a/pages/api/graphql.ts +++ b/pages/api/postgraphql.ts @@ -11,7 +11,7 @@ const requestHandler = postgraphile( [databaseSchemaAdmin, databaseSchemaClean, databaseSchemaWorkspace], { ...options, - graphqlRoute: "/api/graphql", + graphqlRoute: "/api/postgraphql", } ); diff --git a/playwright.config.ts b/playwright.config.ts index cdcccfe..945eb56 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,77 +1,60 @@ -import { defineConfig, devices } from '@playwright/test'; - -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// require('dotenv').config(); +import { PlaywrightTestConfig, devices } from "@playwright/test"; +import path from "path"; + +// Use process.env.PORT by default and fallback to port 3000 +const PORT = process.env.PORT || 3000; + +// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port +const baseURL = `http://localhost:${PORT}`; + +// Reference: https://playwright.dev/docs/test-configuration +const config: PlaywrightTestConfig = { + // Timeout per test + timeout: 30 * 1000, + // Test directory + testDir: path.join(__dirname, "tests"), + // If a test fails, retry it additional 2 times + retries: 2, + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: "test-results/", + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + webServer: { + command: "npm run dev", + port: +PORT, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + }, -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + // Use baseURL so to make navigations relative. + // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url + baseURL, + + // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. + // More information: https://playwright.dev/docs/trace-viewer + trace: "retry-with-trace", - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context + // contextOptions: { + // ignoreHTTPSErrors: true, + // }, }, - /* Configure projects for major browsers */ projects: [ + // Setup project + { name: "setup", testMatch: /.*\.setup\.ts/ }, { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { + ...devices["Desktop Chrome"], + // Use prepared auth state. + storageState: "playwright/.auth/user.json", + }, + dependencies: ["setup"], }, - - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, - - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, - // }, + // Test against mobile viewports... ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, -}); +}; +export default config; diff --git a/playwright/.auth/user.json b/playwright/.auth/user.json new file mode 100644 index 0000000..21030a8 --- /dev/null +++ b/playwright/.auth/user.json @@ -0,0 +1,25 @@ +{ + "cookies": [ + { + "name": "i18next", + "value": "en-US", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": false, + "secure": false, + "sameSite": "Strict" + }, + { + "name": "next-auth.session-token", + "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ", + "domain": "localhost", + "path": "/", + "expires": 1689713869, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25fea30..421bb92 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3,7 +3,7 @@ lockfileVersion: '6.0' dependencies: '@google-cloud/storage': specifier: ^6.11.0 - version: 6.11.0 + version: 6.11.0(encoding@0.1.13) '@graphile-contrib/pg-order-by-related': specifier: ^1.0.0 version: 1.0.0 @@ -12,7 +12,7 @@ dependencies: version: 6.1.0 '@graphile/pg-aggregates': specifier: ^0.1.1 - version: 0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@15.8.0) + version: 0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@16.7.1) '@headlessui/react': specifier: ^1.7.15 version: 1.7.15(react-dom@18.2.0)(react@18.2.0) @@ -30,7 +30,7 @@ dependencies: version: 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@mui/x-data-grid': specifier: latest - version: 6.9.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0) + version: 6.10.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0) '@types/node': specifier: 20.2.0 version: 20.2.0 @@ -46,6 +46,12 @@ dependencies: autoprefixer: specifier: 10.4.14 version: 10.4.14(postcss@8.4.23) + bufferutil: + specifier: ^4.0.7 + version: 4.0.7 + encoding: + specifier: ^0.1.13 + version: 0.1.13 eslint: specifier: 8.40.0 version: 8.40.0 @@ -67,9 +73,12 @@ dependencies: formdata-polyfill: specifier: ^4.0.10 version: 4.0.10 + graphql: + specifier: ^16.7.1 + version: 16.7.1 graphql-request: specifier: ^5.2.0 - version: 5.2.0(graphql@15.8.0) + version: 5.2.0(encoding@0.1.13)(graphql@16.7.1) i18next: specifier: ^22.5.1 version: 22.5.1 @@ -79,21 +88,24 @@ dependencies: i18next-resources-to-backend: specifier: 1.1.3 version: 1.1.3 + mock-express-request: + specifier: ^0.2.2 + version: 0.2.2 mui-datatables: specifier: ^4.3.0 version: 4.3.0(@emotion/react@11.11.1)(@mui/icons-material@5.11.16)(@mui/material@5.13.5)(react-dom@18.2.0)(react@18.2.0) next: specifier: latest - version: 13.4.7(react-dom@18.2.0)(react@18.2.0) + version: 13.4.10(react-dom@18.2.0)(react@18.2.0) next-auth: specifier: ^4.22.1 - version: 4.22.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0) + version: 4.22.1(next@13.4.10)(react-dom@18.2.0)(react@18.2.0) next-i18next: specifier: ^13.3.0 - version: 13.3.0(i18next@22.5.1)(next@13.4.7)(react-i18next@12.1.1)(react@18.2.0) + version: 13.3.0(i18next@22.5.1)(next@13.4.10)(react-i18next@12.1.1)(react@18.2.0) next-runtime-dotenv: specifier: ^1.5.1 - version: 1.5.1(next@13.4.7) + version: 1.5.1(next@13.4.10) pg: specifier: ^8.11.0 version: 8.11.0 @@ -102,7 +114,7 @@ dependencies: version: 8.4.23 postgraphile: specifier: ^4.13.0 - version: 4.13.0 + version: 4.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) postgraphile-plugin-connection-filter: specifier: ^2.3.0 version: 2.3.0 @@ -127,6 +139,9 @@ dependencies: typescript: specifier: 5.0.4 version: 5.0.4 + utf-8-validate: + specifier: ^6.0.3 + version: 6.0.3 devDependencies: '@playwright/test': @@ -401,7 +416,7 @@ packages: engines: {node: '>=12'} dev: false - /@google-cloud/storage@6.11.0: + /@google-cloud/storage@6.11.0(encoding@0.1.13): resolution: {integrity: sha512-p5VX5K2zLTrMXlKdS1CiQNkKpygyn7CBFm5ZvfhVj6+7QUsjWvYx9YDMkYXdarZ6JDt4cxiu451y9QUIH82ZTw==} engines: {node: '>=12'} dependencies: @@ -414,13 +429,13 @@ packages: duplexify: 4.1.2 ent: 2.2.0 extend: 3.0.2 - gaxios: 5.1.2 - google-auth-library: 8.8.0 + gaxios: 5.1.2(encoding@0.1.13) + google-auth-library: 8.8.0(encoding@0.1.13) mime: 3.0.0 mime-types: 2.1.35 p-limit: 3.1.0 retry-request: 5.0.2 - teeny-request: 8.0.3 + teeny-request: 8.0.3(encoding@0.1.13) uuid: 8.3.2 transitivePeerDependencies: - encoding @@ -442,7 +457,7 @@ packages: tslib: 2.5.3 dev: false - /@graphile/pg-aggregates@0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@15.8.0): + /@graphile/pg-aggregates@0.1.1(graphile-build-pg@4.13.0)(graphile-build@4.13.0)(graphql@16.7.1): resolution: {integrity: sha512-bPfniRw4oN9nNP8tkRlbBslNMA38fhVWNhhaReODhPVEshwquzUmSmSCtSVhS4J+StEFgrP7Z+z1IN0/ror2XA==} peerDependencies: graphile-build: ^4.12.0-alpha.0 @@ -452,20 +467,20 @@ packages: '@types/debug': 4.1.8 '@types/graphql': 14.5.0 debug: 4.3.4 - graphile-build: 4.13.0(graphql@15.8.0) - graphile-build-pg: 4.13.0(graphql@15.8.0)(pg@8.11.0) + graphile-build: 4.13.0(graphql@16.7.1) + graphile-build-pg: 4.13.0(graphql@16.7.1)(pg@8.11.0) graphile-utils: 4.13.0(graphile-build-pg@4.13.0)(graphile-build@4.13.0) - graphql: 15.8.0 + graphql: 16.7.1 transitivePeerDependencies: - supports-color dev: false - /@graphql-typed-document-node/core@3.2.0(graphql@15.8.0): + /@graphql-typed-document-node/core@3.2.0(graphql@16.7.1): resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - graphql: 15.8.0 + graphql: 16.7.1 dev: false /@headlessui/react@1.7.15(react-dom@18.2.0)(react@18.2.0): @@ -630,7 +645,7 @@ packages: '@babel/runtime': 7.22.5 '@emotion/is-prop-valid': 1.2.1 '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.13.1(react@18.2.0) + '@mui/utils': 5.13.7(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.6 clsx: 1.2.1 @@ -705,7 +720,7 @@ packages: optional: true dependencies: '@babel/runtime': 7.22.5 - '@mui/utils': 5.13.1(react@18.2.0) + '@mui/utils': 5.13.7(react@18.2.0) '@types/react': 18.2.6 prop-types: 15.8.1 react: 18.2.0 @@ -753,7 +768,7 @@ packages: '@mui/private-theming': 5.13.1(@types/react@18.2.6)(react@18.2.0) '@mui/styled-engine': 5.13.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.13.1(react@18.2.0) + '@mui/utils': 5.13.7(react@18.2.0) '@types/react': 18.2.6 clsx: 1.2.1 csstype: 3.1.2 @@ -783,8 +798,21 @@ packages: react: 18.2.0 react-is: 18.2.0 - /@mui/x-data-grid@6.9.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-JlvLvYHUrfFBQMQa7fzkxUmEVygQL5IWt0cww1Rs4zKYaVcDM5v1T8ZhFOrOQp04c7meB+CykuP94r3Dn30wqQ==} + /@mui/utils@5.13.7(react@18.2.0): + resolution: {integrity: sha512-/3BLptG/q0u36eYED7Nhf4fKXmcKb6LjjT7ZMwhZIZSdSxVqDqSTmATW3a56n3KEPQUXCU9TpxAfCBQhs6brVA==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.5 + '@types/prop-types': 15.7.5 + '@types/react-is': 18.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + + /@mui/x-data-grid@6.10.0(@mui/material@5.13.5)(@mui/system@5.13.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-x9h+Z4B2vu+ZKKwClBVs30Y9eZYdhqyV3toHH2E0zat7FIZxwiVfk6qz4Q98V1fV0Fe1nczPj9i0siUmduMEXg==} engines: {node: '>=14.0.0'} peerDependencies: '@mui/material': ^5.4.1 @@ -795,7 +823,7 @@ packages: '@babel/runtime': 7.22.5 '@mui/material': 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@mui/system': 5.13.5(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) - '@mui/utils': 5.13.1(react@18.2.0) + '@mui/utils': 5.13.7(react@18.2.0) clsx: 1.2.1 prop-types: 15.8.1 react: 18.2.0 @@ -803,8 +831,8 @@ packages: reselect: 4.1.8 dev: false - /@next/env@13.4.7: - resolution: {integrity: sha512-ZlbiFulnwiFsW9UV1ku1OvX/oyIPLtMk9p/nnvDSwI0s7vSoZdRtxXNsaO+ZXrLv/pMbXVGq4lL8TbY9iuGmVw==} + /@next/env@13.4.10: + resolution: {integrity: sha512-3G1yD/XKTSLdihyDSa8JEsaWOELY+OWe08o0LUYzfuHp1zHDA8SObQlzKt+v+wrkkPcnPweoLH1ImZeUa0A1NQ==} dev: false /@next/eslint-plugin-next@13.4.2: @@ -813,8 +841,8 @@ packages: glob: 7.1.7 dev: false - /@next/swc-darwin-arm64@13.4.7: - resolution: {integrity: sha512-VZTxPv1b59KGiv/pZHTO5Gbsdeoxcj2rU2cqJu03btMhHpn3vwzEK0gUSVC/XW96aeGO67X+cMahhwHzef24/w==} + /@next/swc-darwin-arm64@13.4.10: + resolution: {integrity: sha512-4bsdfKmmg7mgFGph0UorD1xWfZ5jZEw4kKRHYEeTK9bT1QnMbPVPlVXQRIiFPrhoDQnZUoa6duuPUJIEGLV1Jg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -822,8 +850,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@13.4.7: - resolution: {integrity: sha512-gO2bw+2Ymmga+QYujjvDz9955xvYGrWofmxTq7m70b9pDPvl7aDFABJOZ2a8SRCuSNB5mXU8eTOmVVwyp/nAew==} + /@next/swc-darwin-x64@13.4.10: + resolution: {integrity: sha512-ngXhUBbcZIWZWqNbQSNxQrB9T1V+wgfCzAor2olYuo/YpaL6mUYNUEgeBMhr8qwV0ARSgKaOp35lRvB7EmCRBg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -831,8 +859,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@13.4.7: - resolution: {integrity: sha512-6cqp3vf1eHxjIDhEOc7Mh/s8z1cwc/l5B6ZNkOofmZVyu1zsbEM5Hmx64s12Rd9AYgGoiCz4OJ4M/oRnkE16/Q==} + /@next/swc-linux-arm64-gnu@13.4.10: + resolution: {integrity: sha512-SjCZZCOmHD4uyM75MVArSAmF5Y+IJSGroPRj2v9/jnBT36SYFTORN8Ag/lhw81W9EeexKY/CUg2e9mdebZOwsg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -840,8 +868,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@13.4.7: - resolution: {integrity: sha512-T1kD2FWOEy5WPidOn1si0rYmWORNch4a/NR52Ghyp4q7KyxOCuiOfZzyhVC5tsLIBDH3+cNdB5DkD9afpNDaOw==} + /@next/swc-linux-arm64-musl@13.4.10: + resolution: {integrity: sha512-F+VlcWijX5qteoYIOxNiBbNE8ruaWuRlcYyIRK10CugqI/BIeCDzEDyrHIHY8AWwbkTwe6GRHabMdE688Rqq4Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -849,8 +877,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@13.4.7: - resolution: {integrity: sha512-zaEC+iEiAHNdhl6fuwl0H0shnTzQoAoJiDYBUze8QTntE/GNPfTYpYboxF5LRYIjBwETUatvE0T64W6SKDipvg==} + /@next/swc-linux-x64-gnu@13.4.10: + resolution: {integrity: sha512-WDv1YtAV07nhfy3i1visr5p/tjiH6CeXp4wX78lzP1jI07t4PnHHG1WEDFOduXh3WT4hG6yN82EQBQHDi7hBrQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -858,8 +886,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@13.4.7: - resolution: {integrity: sha512-X6r12F8d8SKAtYJqLZBBMIwEqcTRvUdVm+xIq+l6pJqlgT2tNsLLf2i5Cl88xSsIytBICGsCNNHd+siD2fbWBA==} + /@next/swc-linux-x64-musl@13.4.10: + resolution: {integrity: sha512-zFkzqc737xr6qoBgDa3AwC7jPQzGLjDlkNmt/ljvQJ/Veri5ECdHjZCUuiTUfVjshNIIpki6FuP0RaQYK9iCRg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -867,8 +895,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@13.4.7: - resolution: {integrity: sha512-NPnmnV+vEIxnu6SUvjnuaWRglZzw4ox5n/MQTxeUhb5iwVWFedolPFebMNwgrWu4AELwvTdGtWjqof53AiWHcw==} + /@next/swc-win32-arm64-msvc@13.4.10: + resolution: {integrity: sha512-IboRS8IWz5mWfnjAdCekkl8s0B7ijpWeDwK2O8CdgZkoCDY0ZQHBSGiJ2KViAG6+BJVfLvcP+a2fh6cdyBr9QQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -876,8 +904,8 @@ packages: dev: false optional: true - /@next/swc-win32-ia32-msvc@13.4.7: - resolution: {integrity: sha512-6Hxijm6/a8XqLQpOOf/XuwWRhcuc/g4rBB2oxjgCMuV9Xlr2bLs5+lXyh8w9YbAUMYR3iC9mgOlXbHa79elmXw==} + /@next/swc-win32-ia32-msvc@13.4.10: + resolution: {integrity: sha512-bSA+4j8jY4EEiwD/M2bol4uVEu1lBlgsGdvM+mmBm/BbqofNBfaZ2qwSbwE2OwbAmzNdVJRFRXQZ0dkjopTRaQ==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] @@ -885,8 +913,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@13.4.7: - resolution: {integrity: sha512-sW9Yt36Db1nXJL+mTr2Wo0y+VkPWeYhygvcHj1FF0srVtV+VoDjxleKtny21QHaG05zdeZnw2fCtf2+dEqgwqA==} + /@next/swc-win32-x64-msvc@13.4.10: + resolution: {integrity: sha512-g2+tU63yTWmcVQKDGY0MV1PjjqgZtwM4rB1oVVi/v0brdZAcrcTV+04agKzWtvWroyFz6IqtT0MoZJA7PNyLVw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -982,7 +1010,7 @@ packages: resolution: {integrity: sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA==} deprecated: This is a stub types definition. graphql provides its own type definitions, so you do not need this installed. dependencies: - graphql: 16.6.0 + graphql: 16.7.1 dev: false /@types/hoist-non-react-statics@3.3.1: @@ -1147,6 +1175,14 @@ packages: stable: 0.1.8 dev: false + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1434,6 +1470,14 @@ packages: engines: {node: '>=4'} dev: false + /bufferutil@4.0.7: + resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.0 + dev: false + /bundle-name@3.0.0: resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} engines: {node: '>=12'} @@ -1595,10 +1639,10 @@ packages: path-type: 4.0.0 yaml: 1.10.2 - /cross-fetch@3.1.6: + /cross-fetch@3.1.6(encoding@0.1.13): resolution: {integrity: sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==} dependencies: - node-fetch: 2.6.11 + node-fetch: 2.6.11(encoding@0.1.13) transitivePeerDependencies: - encoding dev: false @@ -1808,6 +1852,12 @@ packages: engines: {node: '>= 0.8'} dev: false + /encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + dependencies: + iconv-lite: 0.6.3 + dev: false + /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} dependencies: @@ -2394,6 +2444,11 @@ packages: tslib: 2.5.3 dev: false + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: false @@ -2422,24 +2477,24 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: false - /gaxios@5.1.2: + /gaxios@5.1.2(encoding@0.1.13): resolution: {integrity: sha512-mPyw3qQq6qoHWTe27CrzhSj7XYKVStTGrpP92a91FfogBWOd9BMW8GT5yS5WhEYGw02AgB1fVQVSAO+JKiQP0w==} engines: {node: '>=12'} dependencies: extend: 3.0.2 https-proxy-agent: 5.0.1 is-stream: 2.0.1 - node-fetch: 2.6.11 + node-fetch: 2.6.11(encoding@0.1.13) transitivePeerDependencies: - encoding - supports-color dev: false - /gcp-metadata@5.2.0: + /gcp-metadata@5.2.0(encoding@0.1.13): resolution: {integrity: sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==} engines: {node: '>=12'} dependencies: - gaxios: 5.1.2 + gaxios: 5.1.2(encoding@0.1.13) json-bigint: 1.0.0 transitivePeerDependencies: - encoding @@ -2562,7 +2617,7 @@ packages: slash: 4.0.0 dev: false - /google-auth-library@8.8.0: + /google-auth-library@8.8.0(encoding@0.1.13): resolution: {integrity: sha512-0iJn7IDqObDG5Tu9Tn2WemmJ31ksEa96IyK0J0OZCpTh6CrC6FrattwKX87h3qKVuprCJpdOGKc1Xi8V0kMh8Q==} engines: {node: '>=12'} dependencies: @@ -2570,9 +2625,9 @@ packages: base64-js: 1.5.1 ecdsa-sig-formatter: 1.0.11 fast-text-encoding: 1.0.6 - gaxios: 5.1.2 - gcp-metadata: 5.2.0 - gtoken: 6.1.2 + gaxios: 5.1.2(encoding@0.1.13) + gcp-metadata: 5.2.0(encoding@0.1.13) + gtoken: 6.1.2(encoding@0.1.13) jws: 4.0.0 lru-cache: 6.0.0 transitivePeerDependencies: @@ -2612,7 +2667,27 @@ packages: chalk: 2.4.2 debug: 4.3.4 graphile-build: 4.13.0(graphql@15.8.0) - jsonwebtoken: 9.0.0 + jsonwebtoken: 9.0.1 + lodash: 4.17.21 + lru-cache: 4.1.5 + pg: 8.11.0 + pg-sql2: 4.13.0(pg@8.11.0) + transitivePeerDependencies: + - graphql + - supports-color + dev: false + + /graphile-build-pg@4.13.0(graphql@16.7.1)(pg@8.11.0): + resolution: {integrity: sha512-1FD+3wjCdK1lbICY1QVO26A7s8efSjR522LarL9Bx1M1iBJHNIpCEW2PK+LkulQjY1l5LGQ1A93GQFqi6cZ6bg==} + engines: {node: '>=8.6'} + peerDependencies: + pg: '>=6.1.0 <9' + dependencies: + '@graphile/lru': 4.11.0 + chalk: 2.4.2 + debug: 4.3.4 + graphile-build: 4.13.0(graphql@16.7.1) + jsonwebtoken: 9.0.1 lodash: 4.17.21 lru-cache: 4.1.5 pg: 8.11.0 @@ -2642,6 +2717,26 @@ packages: - supports-color dev: false + /graphile-build@4.13.0(graphql@16.7.1): + resolution: {integrity: sha512-KPBrHgRw5fury6l9WEQH6ys1UtnxrRrG+Ehnr68NvfNELp4T+QsekTSVFi5LWoJOaXvdYMqP2L8MFBRQP2vKsw==} + engines: {node: '>=8.6'} + peerDependencies: + graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0' + dependencies: + '@graphile/lru': 4.11.0 + chalk: 2.4.2 + debug: 4.3.4 + graphql: 16.7.1 + graphql-parse-resolve-info: 4.13.0(graphql@16.7.1) + iterall: 1.3.0 + lodash: 4.17.21 + lru-cache: 5.1.1 + pluralize: 7.0.0 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /graphile-utils@4.13.0(graphile-build-pg@4.13.0)(graphile-build@4.13.0): resolution: {integrity: sha512-6nzlCNeJB1qV9AaPyJ/iHU+CDfs8jxpcmQ47Fmrgmp8r5VwKdL/uDt0LW8IuXu2VZrbM1GGyZ8rQtcdVmQYZ+g==} engines: {node: '>=8.6'} @@ -2650,8 +2745,8 @@ packages: graphile-build-pg: ^4.5.0 dependencies: debug: 4.3.4 - graphile-build: 4.13.0(graphql@15.8.0) - graphile-build-pg: 4.13.0(graphql@15.8.0)(pg@8.11.0) + graphile-build: 4.13.0(graphql@16.7.1) + graphile-build-pg: 4.13.0(graphql@16.7.1)(pg@8.11.0) graphql: 15.8.0 tslib: 2.5.3 transitivePeerDependencies: @@ -2671,16 +2766,29 @@ packages: - supports-color dev: false - /graphql-request@5.2.0(graphql@15.8.0): + /graphql-parse-resolve-info@4.13.0(graphql@16.7.1): + resolution: {integrity: sha512-VVJ1DdHYcR7hwOGQKNH+QTzuNgsLA8l/y436HtP9YHoX6nmwXRWq3xWthU3autMysXdm0fQUbhTZCx0W9ICozw==} + engines: {node: '>=8.6'} + peerDependencies: + graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0 || ^16.3.0' + dependencies: + debug: 4.3.4 + graphql: 16.7.1 + tslib: 2.5.3 + transitivePeerDependencies: + - supports-color + dev: false + + /graphql-request@5.2.0(encoding@0.1.13)(graphql@16.7.1): resolution: {integrity: sha512-pLhKIvnMyBERL0dtFI3medKqWOz/RhHdcgbZ+hMMIb32mEPa5MJSzS4AuXxfI4sRAu6JVVk5tvXuGfCWl9JYWQ==} peerDependencies: graphql: 14 - 16 dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) - cross-fetch: 3.1.6 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.7.1) + cross-fetch: 3.1.6(encoding@0.1.13) extract-files: 9.0.0 form-data: 3.0.1 - graphql: 15.8.0 + graphql: 16.7.1 transitivePeerDependencies: - encoding dev: false @@ -2699,16 +2807,16 @@ packages: engines: {node: '>= 10.x'} dev: false - /graphql@16.6.0: - resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==} + /graphql@16.7.1: + resolution: {integrity: sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} dev: false - /gtoken@6.1.2: + /gtoken@6.1.2(encoding@0.1.13): resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} engines: {node: '>=12.0.0'} dependencies: - gaxios: 5.1.2 + gaxios: 5.1.2(encoding@0.1.13) google-p12-pem: 4.0.1 jws: 4.0.0 transitivePeerDependencies: @@ -2855,6 +2963,13 @@ packages: safer-buffer: 2.1.2 dev: false + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -3118,8 +3233,8 @@ packages: hasBin: true dev: false - /jsonwebtoken@9.0.0: - resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + /jsonwebtoken@9.0.1: + resolution: {integrity: sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==} engines: {node: '>=12', npm: '>=6'} dependencies: jws: 3.2.2 @@ -3333,6 +3448,22 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + /mock-express-request@0.2.2: + resolution: {integrity: sha512-EymHjY1k1jWIsaVaCsPdFterWO18gcNwQMb99OryhSBtIA33SZJujOLeOe03Rf2DTV997xLPyl2I098WCFm/mA==} + dependencies: + accepts: 1.3.8 + fresh: 0.5.2 + lodash: 4.17.21 + mock-req: 0.2.0 + parseurl: 1.3.3 + range-parser: 1.2.1 + type-is: 1.6.18 + dev: false + + /mock-req@0.2.0: + resolution: {integrity: sha512-IUuwS0W5GjoPyjhuXPQJXpaHfHW7UYFRia8Cchm/xRuyDDclpSQdEoakt3krOpSYvgVlQsbnf0ePDsTRDfp7Dg==} + dev: false + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -3399,7 +3530,12 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: false - /next-auth@4.22.1(next@13.4.7)(react-dom@18.2.0)(react@18.2.0): + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /next-auth@4.22.1(next@13.4.10)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-NTR3f6W7/AWXKw8GSsgSyQcDW6jkslZLH8AiZa5PQ09w1kR8uHtR9rez/E9gAq/o17+p0JYHE8QjF3RoniiObA==} peerDependencies: next: ^12.2.5 || ^13 @@ -3414,7 +3550,7 @@ packages: '@panva/hkdf': 1.1.1 cookie: 0.5.0 jose: 4.14.4 - next: 13.4.7(react-dom@18.2.0)(react@18.2.0) + next: 13.4.10(react-dom@18.2.0)(react@18.2.0) oauth: 0.9.15 openid-client: 5.4.2 preact: 10.15.1 @@ -3424,7 +3560,7 @@ packages: uuid: 8.3.2 dev: false - /next-i18next@13.3.0(i18next@22.5.1)(next@13.4.7)(react-i18next@12.1.1)(react@18.2.0): + /next-i18next@13.3.0(i18next@22.5.1)(next@13.4.10)(react-i18next@12.1.1)(react@18.2.0): resolution: {integrity: sha512-X4kgi51BCOoGdKbv87eZ8OU7ICQDg5IP+T5fNjqDY3os9ea0OKTY4YpAiVFiwcI9XimcUmSPbKO4a9jFUyYSgg==} engines: {node: '>=14'} peerDependencies: @@ -3439,22 +3575,22 @@ packages: hoist-non-react-statics: 3.3.2 i18next: 22.5.1 i18next-fs-backend: 2.1.5 - next: 13.4.7(react-dom@18.2.0)(react@18.2.0) + next: 13.4.10(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-i18next: 12.1.1(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) dev: false - /next-runtime-dotenv@1.5.1(next@13.4.7): + /next-runtime-dotenv@1.5.1(next@13.4.10): resolution: {integrity: sha512-G1NWW06geegqev1U3E90lfYYMV+xvVIwyQv2KbQCRp03jSdUbRANcEm/QQnNoJqEGQUXoEgYDdSV6jB0yv/xAQ==} peerDependencies: next: '>= 5.1.0' dependencies: dotenv: 16.1.4 - next: 13.4.7(react-dom@18.2.0)(react@18.2.0) + next: 13.4.10(react-dom@18.2.0)(react@18.2.0) dev: false - /next@13.4.7(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-M8z3k9VmG51SRT6v5uDKdJXcAqLzP3C+vaKfLIAM0Mhx1um1G7MDnO63+m52qPdZfrTFzMZNzfsgvm3ghuVHIQ==} + /next@13.4.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-4ep6aKxVTQ7rkUW2fBLhpBr/5oceCuf4KmlUpvG/aXuDTIf9mexNSpabUD6RWPspu6wiJJvozZREhXhueYO36A==} engines: {node: '>=16.8.0'} hasBin: true peerDependencies: @@ -3471,7 +3607,7 @@ packages: sass: optional: true dependencies: - '@next/env': 13.4.7 + '@next/env': 13.4.10 '@swc/helpers': 0.5.1 busboy: 1.6.0 caniuse-lite: 1.0.30001503 @@ -3482,15 +3618,15 @@ packages: watchpack: 2.4.0 zod: 3.21.4 optionalDependencies: - '@next/swc-darwin-arm64': 13.4.7 - '@next/swc-darwin-x64': 13.4.7 - '@next/swc-linux-arm64-gnu': 13.4.7 - '@next/swc-linux-arm64-musl': 13.4.7 - '@next/swc-linux-x64-gnu': 13.4.7 - '@next/swc-linux-x64-musl': 13.4.7 - '@next/swc-win32-arm64-msvc': 13.4.7 - '@next/swc-win32-ia32-msvc': 13.4.7 - '@next/swc-win32-x64-msvc': 13.4.7 + '@next/swc-darwin-arm64': 13.4.10 + '@next/swc-darwin-x64': 13.4.10 + '@next/swc-linux-arm64-gnu': 13.4.10 + '@next/swc-linux-arm64-musl': 13.4.10 + '@next/swc-linux-x64-gnu': 13.4.10 + '@next/swc-linux-x64-musl': 13.4.10 + '@next/swc-win32-arm64-msvc': 13.4.10 + '@next/swc-win32-ia32-msvc': 13.4.10 + '@next/swc-win32-x64-msvc': 13.4.10 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -3501,7 +3637,7 @@ packages: engines: {node: '>=10.5.0'} dev: false - /node-fetch@2.6.11: + /node-fetch@2.6.11(encoding@0.1.13): resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} engines: {node: 4.x || >=6.0.0} peerDependencies: @@ -3510,6 +3646,7 @@ packages: encoding: optional: true dependencies: + encoding: 0.1.13 whatwg-url: 5.0.0 dev: false @@ -3518,6 +3655,11 @@ packages: engines: {node: '>= 6.13.0'} dev: false + /node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + dev: false + /node-releases@2.0.12: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: false @@ -3985,7 +4127,7 @@ packages: tslib: 2.5.3 dev: false - /postgraphile@4.13.0: + /postgraphile@4.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-p2VqUnsECd1XrucylK1iosvKEn96J8CWeMVWzxF7b6G21jmaETvFe2CO2q4+dKY5DFCVEF2O9pEfmUfYCKl5+A==} engines: {node: '>=8.6'} hasBin: true @@ -4008,15 +4150,15 @@ packages: http-errors: 1.8.1 iterall: 1.3.0 json5: 2.2.3 - jsonwebtoken: 9.0.0 + jsonwebtoken: 9.0.1 parseurl: 1.3.3 pg: 8.11.0 pg-connection-string: 2.6.0 pg-sql2: 4.13.0(pg@8.11.0) postgraphile-core: 4.13.0(graphql@15.8.0)(pg@8.11.0) - subscriptions-transport-ws: 0.9.19(graphql@15.8.0) + subscriptions-transport-ws: 0.9.19(bufferutil@4.0.7)(graphql@15.8.0)(utf-8-validate@6.0.3) tslib: 2.5.3 - ws: 7.5.9 + ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - pg-native @@ -4128,6 +4270,11 @@ packages: performance-now: 2.1.0 dev: false + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + /raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -4614,7 +4761,7 @@ packages: /stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - /subscriptions-transport-ws@0.9.19(graphql@15.8.0): + /subscriptions-transport-ws@0.9.19(bufferutil@4.0.7)(graphql@15.8.0)(utf-8-validate@6.0.3): resolution: {integrity: sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==} deprecated: The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md peerDependencies: @@ -4625,7 +4772,7 @@ packages: graphql: 15.8.0 iterall: 1.3.0 symbol-observable: 1.2.0 - ws: 7.5.9 + ws: 7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -4720,13 +4867,13 @@ packages: engines: {node: '>=6'} dev: false - /teeny-request@8.0.3: + /teeny-request@8.0.3(encoding@0.1.13): resolution: {integrity: sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==} engines: {node: '>=12'} dependencies: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 - node-fetch: 2.6.11 + node-fetch: 2.6.11(encoding@0.1.13) stream-events: 1.0.5 uuid: 9.0.0 transitivePeerDependencies: @@ -4894,6 +5041,14 @@ packages: punycode: 2.3.0 dev: false + /utf-8-validate@6.0.3: + resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: false @@ -4976,7 +5131,7 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: false - /ws@7.5.9: + /ws@7.5.9(bufferutil@4.0.7)(utf-8-validate@6.0.3): resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} engines: {node: '>=8.3.0'} peerDependencies: @@ -4987,6 +5142,9 @@ packages: optional: true utf-8-validate: optional: true + dependencies: + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 dev: false /xtend@4.0.2: diff --git a/public/next.svg b/public/next.svg index 5174b28..5bb00d4 100644 --- a/public/next.svg +++ b/public/next.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/thirteen.svg b/public/thirteen.svg index 8977c1b..db65b53 100644 --- a/public/thirteen.svg +++ b/public/thirteen.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/public/vercel.svg b/public/vercel.svg index d2f8422..1aeda7d 100644 --- a/public/vercel.svg +++ b/public/vercel.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/tailwind.config.js b/tailwind.config.js index 8e59cb7..47d3b2c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -11,4 +11,4 @@ module.exports = { }, }, plugins: [require("@headlessui/tailwindcss")], -}; \ No newline at end of file +}; diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 2fd6016..0000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -]; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} diff --git a/tests/.env b/tests/.env new file mode 100644 index 0000000..2010c16 --- /dev/null +++ b/tests/.env @@ -0,0 +1,3 @@ +API_HOST=http://localhost:3000/ +NEXTAUTH_JWT_ANALYST= "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ" +GOOGLE_BUCKET_NAME=eed_upload_file_storage diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts new file mode 100644 index 0000000..5180065 --- /dev/null +++ b/tests/auth.setup.ts @@ -0,0 +1,70 @@ +import { test as setup, chromium } from "@playwright/test"; +const dotenv = require("dotenv"); +dotenv.config({ + path: "./tests/.env", +}); + +const siteUrl = process.env.API_HOST as string; +const authFile = "playwright/.auth/user.json"; +// ๐Ÿ‘‡๏ธ hard-coded JWT obtained from app/utils/postgraphile/helpers.tsx-possibly explore jsonwebtoken to encrypt a JSON mock response +const jwt = process.env.NEXTAUTH_JWT_ANALYST as string; + +// ๐Ÿ‘‡๏ธ setup function defines a test case that will run before all other test cases within the test +setup("authenticate", async () => { + // ๐Ÿ‘‡๏ธ set up a Playwright browser instance: + const browser = await chromium.launch(); + const context = await browser.newContext(); + const page = await context.newPage(); + + // ๐Ÿ‘‡๏ธ navigate to the page where the session token needs to be mocked + await page.goto(`${siteUrl}`); + + // ๐Ÿ‘‡๏ธ add token as next-auth cookie + await context.addCookies([ + { + name: "next-auth.session-token", + value: jwt, + domain: "localhost", + path: "/", + httpOnly: true, + sameSite: "Lax", + expires: Math.floor((Date.now() + 24 * 60 * 60 * 1000) / 1000), + }, + ]); + + // ๐Ÿ‘‡๏ธ store the browser's signed state in the specified file path + // โ— this allows you to persist the authentication state between tests or test runs + await context.storageState({ path: authFile }); + await browser.close(); +}); + +/* + // ๐Ÿ‘‡๏ธ Google/MS authentication... + await page.locator('button[data-myprovider="Google"]').click(); + // click redirects page to Google auth form + await page.fill('input[type="email"]', email); + await page.getByRole("button", { name: "Next" }).click(); + // with @button.is email, click redirects page to MicroSoft auth form + await page.fill('input[type="email"]', email); + await page.getByRole("button", { name: "Next" }).click(); + await page.locator("#i0118").fill(password); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.getByRole("button", { name: "Yes" }).click(); + // โ— Navigation failed... + await page.waitForURL(`${siteUrl}/en/analyst/home`); + + + + // ๐Ÿ‘‡๏ธ mock JSON user session... + const mockSession = { + user: { + id: "user-id", + name: name, + email: email, + role: role, + }, + }; + + // Encrypt the mock response + const jwt = jwt.sign(mockResponse, secret); +*/ diff --git a/tests/gcs/file-upload.spec.ts b/tests/gcs/file-upload.spec.ts new file mode 100644 index 0000000..6dca4c9 --- /dev/null +++ b/tests/gcs/file-upload.spec.ts @@ -0,0 +1,183 @@ +//record test: npx playwright codegen http://localhost:3000/en/analyst/dataset/add +//run tests: pnpm run test:gcs + +//AC: https://www.notion.so/buttoninc/Playwright-Tests-for-GSC-ba1f819ac78d4d1ea0913664330a4f1c?pvs=4 + +import { test, expect, chromium, Page, BrowserContext } from "@playwright/test"; +import path from "path"; +import { Storage } from "@google-cloud/storage"; + +const dotenv = require("dotenv"); +dotenv.config({ + path: "./tests/.env", +}); + +// ๐Ÿ‘‡๏ธ Test parameters +const siteUrl: string | undefined = process.env.API_HOST; +const fileNames = [ + "helloWorld.json", + "helloWorld.xml", + "helloWorld.csv", + "helloWorld.xls", + "helloWorld.xlsx", +]; +const filePaths = fileNames.map((fileName) => + path.resolve(process.cwd(), "tests", "gcs", "files", fileName) +); + +const fileFailPath = path.resolve( + process.cwd(), + "tests", + "gcs", + "files", + "helloWorld.ods" +); + +const lngs = ["en", "fr"]; +const successMessageDictionary: { [key: string]: string } = { + en: "Success", + fr: "Succรจs", +}; + +// Create a new instance of the Storage class +const storage = new Storage(); +const bucketName = process.env.GOOGLE_BUCKET_NAME as string; + +// ๐Ÿ‘‡๏ธ Test fixtures +interface TestFixtures { + browser: any; + context: BrowserContext; + page: Page; +} + +// ๐Ÿ‘‡๏ธ Common assertions +async function assertIsMaskedDivHidden(page: Page) { + await page.waitForSelector("div.--is-masked", { state: "hidden" }); + const isMaskedDiv = await page.$("div.--is-masked"); + const isMaskedDivVisible = isMaskedDiv + ? await isMaskedDiv.isVisible() + : false; + expect(isMaskedDivVisible).toBe(false); +} + +// ๐Ÿ‘‡๏ธ Test case: File Upload to GCS bucket +test.describe("File Upload to GCS bucket", () => { + test.beforeAll(async () => { + return { + browser: await chromium.launch(), + }; + }); + + test.afterAll(async ({ browser }) => { + await browser.close(); + }); + + test.beforeEach(async ({ browser }) => { + const context = await browser.newContext(); + const page = await context.newPage(); + + return { + context, + page, + }; + }); + + test.afterEach(async ({ context }) => { + await context.close(); + }); + + // ๐Ÿ‘‡๏ธ Loop over language paths and file paths + for (const lng of lngs) { + for (const filePath of filePaths) { + // ๐Ÿ‘‡๏ธ Test case: File Upload - supported file + test(`Add dataset: file input event - onchange success (${lng}) - ${path.basename( + filePath + )}`, async ({ page }: TestFixtures) => { + await page.goto(`${siteUrl}/${lng}/analyst/dataset/add`); + + const divElement = await page.waitForSelector("div[data-myFileInput]"); + + const fileChooserPromise = new Promise((resolve) => { + page.on("filechooser", async (fileChooser) => { + await fileChooser.setFiles(filePath); + resolve(); + }); + }); + + await divElement.click(); + await fileChooserPromise; + + // ๐Ÿ‘‡๏ธ Assert the success message text + await page.waitForSelector(".bg-green-100"); + const successMessageSelector = ".bg-green-100 p:nth-child(1)"; + const successMessage = await page.textContent(successMessageSelector); + const expectedSuccessMessage = successMessageDictionary[lng]; + expect(successMessage).toContain(expectedSuccessMessage); + + // ๐Ÿ‘‡๏ธ Assert the window mask is hidden + await assertIsMaskedDivHidden(page); + + // ๐Ÿ‘‡๏ธ Assert the file exists in the GCP Storage bucket + const fileName = path.basename(filePath); + const [fileExists] = await storage + .bucket(bucketName) + .file(fileName) + .exists(); + expect(fileExists).toBe(true); + }); + } + } + // ๐Ÿ‘‡๏ธ Test case: File Upload - Failing file + test("Add dataset: file input event - failing file", async ({ + page, + }: TestFixtures) => { + await page.goto(`${siteUrl}/en/analyst/dataset/add`); + + const divElement = await page.waitForSelector("div[data-myFileInput]"); + + const fileChooserPromise = new Promise((resolve) => { + page.on("filechooser", async (fileChooser) => { + await fileChooser.setFiles(fileFailPath); + resolve(); + }); + }); + + await divElement.click(); + await fileChooserPromise; + + // ๐Ÿ‘‡๏ธ Assert the error message text + await page.waitForSelector(".bg-orange-100"); + const errorMessageSelector = ".bg-orange-100 p:nth-child(1)"; + const errorMessage = await page.textContent(errorMessageSelector); + expect(errorMessage).toContain("Error"); + + // ๐Ÿ‘‡๏ธ Assert the window mask is hidden + await assertIsMaskedDivHidden(page); + }); + + // ๐Ÿ‘‡๏ธ Test case: File Upload - Cancel + test("Add dataset: file input event - cancel", async ({ + page, + }: TestFixtures) => { + await page.goto(`${siteUrl}/en/analyst/dataset/add`); + + const divElement = await page.waitForSelector("div[data-myFileInput]"); + + const fileChooserPromise = new Promise((resolve) => { + page.on("filechooser", async (fileChooser) => { + await page.evaluate(() => { + document.dispatchEvent( + new KeyboardEvent("keydown", { key: "Escape" }) + ); + }); + resolve(); + }); + }); + + await divElement.click(); + await fileChooserPromise; + + // ๐Ÿ‘‡๏ธ Assert the window mask is hidden + await assertIsMaskedDivHidden(page); + }); +}); diff --git a/tests/gcs/files/helloWorld.csv b/tests/gcs/files/helloWorld.csv new file mode 100644 index 0000000..590f07a --- /dev/null +++ b/tests/gcs/files/helloWorld.csv @@ -0,0 +1,6 @@ +HelloField,WorldField,IntField +HelloValue1,WorldValue1,1 +HelloValue1,WorldValue2,1 +HelloValue2,WorldValue2,2 +HelloValue2,WorldValue3,2 +HelloValue3,WorldValue4,2 diff --git a/tests/gcs/files/helloWorld.json b/tests/gcs/files/helloWorld.json new file mode 100644 index 0000000..053bba0 --- /dev/null +++ b/tests/gcs/files/helloWorld.json @@ -0,0 +1,15 @@ +{ + "memberHelloWorld": "Hello World", + "memberArray": ["Hello", "World", "Array"], + "memberValue": { + "memberString": "Hello World String", + "memberNumber": 1, + "memberArray": ["Hello", "World", "Array", "in", "Object"], + "memberTrue": true, + "memberFalse": false, + "memberNull": null, + "memberObject": { + "member": "Hello World as member of an object in an object" + } + } +} diff --git a/tests/gcs/files/helloWorld.ods b/tests/gcs/files/helloWorld.ods new file mode 100644 index 0000000000000000000000000000000000000000..f454b2aac8deda00a1ff7306d07cb6ada6734c99 GIT binary patch literal 7963 zcmc&(bzD?yw;nnSlm;o0kVa{c21zN024UzJhM^ltL54;`N=jl#=@jX1krGfsI)+vR z?r^^ALH*Qw&R=)UZ~x}qGkZPjt-ap0)}x|;hE50oU;_ZWNeGQVus{eq0020@kUs%z zENx63-Jqr*C=_gI405!zg>X1SOxSHf4weq=wop@uiLJ4djVZ*D-2rNE3Nmr9Fg0~l z`2iCN^S4kSTavaAGfQ(P`@hf}?s9;PjZML($Xi=`j^Ari|3cH!24rsPz#(DjXaj;e z{6U%g7s`&dwqPg-Vha9)-mkg~wYN35H+66@0{yAGzq07y2y%4#KlDg{)6+j&`<0d% z*cRky`fvLPwS_uCk?ZO7hgtcRwR4q=K4N2I|FxZvgZW!7BU?5gh^3jSgCo1WiCJGv zAH<*YTEN!5K-2zlF%Aq2``g_$RxjT0Jki47*h$2nc^w|3p_&C>eerg_=swqMczMzW zh@ul#n%e~AHfEylmbIu@VG*ojuX#BoI5}R)LY+v7u=0QTg1OcO!Yp7A7s$qU89Y#y zaoaU<_nvY+ZPO^fo$TfI@^EY%sxS%5{RK~Qf&MuhGfh>;wX{gLLmFY&v|wIZDK4l| zts$tACdyYV3`em$z|o(gR!O0aD{fa+pecQ0uEVfl#6RPn;WkV7!1zq?Csu{-J{zAoFG>h^o{vjM-1e-lds<4rdKX^9TT)vZyjglJblI1kDXrK zmVtQil{vjpoXeQbR@Iz`D=a2_+6&h7@DLI-bf^&XA8kK00I5V*52MzmetMJ5ra3+ zFVegENXwD87yfwD;y%cYWfejoFH<5n4TgmbFaJ^i@Q?2_j>|jF%Q1*K?UIz;O)E z{5{344KK#n;4-)4DmS82sKC)=gvuG(`BL8UZitCRF6I3{E+sDVO>l5@1DiUWFJ_yL zjqMcgwbR!Ipz1fTnW8r;73y2X;|td+c}XX)9%~H}(i6Ot^39shYV_{l(~hPOv`bXP zNDMjLp4oB*x_i6nmULaY9=)?;Bq$`8O`v9!I()Y=;dJ-2jiX?L-U+n7EYzq}QDBBR z3m64-3GKuIvoDt7OEuG~J?YSRrVvgS+k;Oe#ZL5SC*+y;+75JJIJja>t2?kZ0rCtU zXIDS9q2vrBVBa~~?a&&bm4>LV4`oamS6s`_ZW@SLP-m5G3*1>I9Q$C!lHmK2pCQzu z&E%fY^U-7kCJvERC<?h<2aY9* z2>t`p(CaM~uLMc_`bIy5=T7J5rlf^n<8cH_q?dA6GOfl9?N}Q?`uh9(jU@L6LxXT@ zVp+S7#IT#HRj1ZH!gy`d{q*H3@y(*1F!s{gm|oHe!W!~Ps_U_N#XzgYtG3LER|;(b z&DOI6J6pZ7VfCA;WV3O38ys;rY6^qX5(5$@D#3RA+wHE~{YGe3#B0k>wDGM&JC>G=2Xa{N08pG{ZD zKEO;QUCaug5u1;5QH#pJ$jfd1uz70M z3roK{n?VsLYe~$xM@BEUl+QQ9K7}oh8{3dRfmQ*)D>q^tQrIcCy`gMJEBtZwi%N-z zGg%E~?xkC$Pxf#N^KMKM>BgK^^Qvy)EUmsz_1px2+r~|;?{4@oY!;@yw2Ts1SFluy zMJEbNqe>Nd9fay#@kKF+i-l_DYdDSYN(4Cdt+sZ=iK_0l%IfoE5Jk2h$y2x58s5h% z_I*VYOr-(+VdLIzG8V6W`x`#7QYtd`>u+!Y9^QiUG?8#3pMd72Fz(q#raNjxy|+K-RYljDP> zt%;e^VK3>hxG@$Ivuo_5R@bELcL?6jNZH>X>4J?&oso&GkzX+frEX)_FY3k4c06p6 z7dWY-jA-suL_m+pv&JSqUHjTlaGw)Vnbdhz%$a+X1if6{ z6EGA-2tH#Be5)a%-geUO=1ypv^X?8$(5t+f$YI&&vEmrI_u6A7`BL+)9t+2xm6Tehpk;ynQYfj!WwGy5)T2(5cW+%2U-a01?=p+LM7>~Ltve03ttpRjr#bWGKgR=6SF z^X7cQ;aj*BH6Op+p=q>LeHdUIb=~Te(msh7^NMovp?*T&!KN7UMZ$M^omQi11NJ3s zi()*oSfKMWnb#c+`4j|Cc;F!xWYE6H5&vN?6gRb5gn_nVhNK&F6E=;<31RcUg!a`U zNc-{71V3KOsPy9mt{&M1F%aFz+J^MU!HrQKP5H-(%B#g5h?4tCu}foW2)ImJr65B5 z@`1QWDEt^s&~tvcyG%k^VTu1Vo8L&`EizQ*l>_sTUlTQfA{F9eghK#2%z~>bZ`lIq!`;m9M1!B zq>OvcK4g3`!r)i8C!bRZ>N*yONC`D}$KC9*+u2g+iWpDjw=R!IXAq61rp{QO?r_M? zARuLNI<42@(1c;z7X|i?EVa!Vh{SZXb)4V{rO-u7vT}29by9v5bUC6cx9$B$!|_JK90~LiL9zbcqH++7B0S@EcAFpk-}mmpAXm3n0m>}bh1OTFF5yM z-2$c*c5Ri|y-df(QXa!0G?@-4T0y*&*#prjc#)cFF<3b$m9+kT!5@d4CpX5=0pz2i4qFNN%?*MKW=!ud#!%+qz-mOLPlW$JW81Z=Gfo1Zj9U6!}G`JfJ;Fwx40cCpM@Fh`oN?k17G@mj0zy=rLI z(cZV~eKq>?)6D5?phs>asTP2JyfuCQ{SbFW znC2JJNQlnNE~iCfKZ9njK{Iqmw(W}8SWn%8oN24B6pq4j^9;6}Ky~9)TbLD^3PR$3TiA3U zieNy|y0<2EkP^k{KfXMT}GRUnm#7{HCizp)55rv-u!_Pr3sqsP0Eg| z$q7-Q;1w@S4t!;ogl-GJ&Vho#gZS%3#4}48a@J>f6`~a*H?{Uw4n;Gnlw;nvzY$n4 zBMMjAFOMo4wxn?FlG`5P5&C-7{-gUehIMrirGGq(xy*A&=O8fajU-OHT#0JNyg$l8 z7zH2)pYqFKVF%_TxsL-gW;JfE3)&xMGr9Wu6z#Zb$*)Vwy-+XQX};CD7c65}@pZeT z>BAvTJaI64a&H2ym$kWk!obw4sd`<|6Ta)DY9)lSHV78F0wClWkR$!e0l_eFh(5VU z7~ffuUop}*pb9fdzW*q!E+{}6Hka16SSz$`?Stci4n5YVoHWpSiMtf41r;&sSzYCd zs-X#?6#KX%pHM1W+J4kbE<5CZvw8(#+)`Yin=8-U^!)mP&b|(r_YFh+3O;4%G$Atto`^W^Mexdht})6Y5t`j_5f@rj9E_05k%f4O(fEiK1k9}xoDc>k$%J`(9bd8TeLd1IW_D+xQ-$pS-d$T@6IZL3HVe;&jRO%R(Q7nISwM-c#u4R-L9f^x zCX4<7rXj?UsLTsxL>vIF>+9X3B}-`h3}|_9rdu7me}E9!8KYfDyi9f!QuGE!s!q^M z5*cxVFYQ(3a+NQcOK4nM~9h_Hp3Mm)HZpV*OI8~bdaAawLnD>lYSxZd?opHaz@uB*E1I4MWWo-ypb4p6$H zw`zP0VxxrFT4?aw z)Q+1k4DX`iTU>{<1vAmr4r_@u##Ox<=3xI|(@b`EMf}v$wlx^^RW=_hju~5@QWQ}R zjT^R%psS0cVjLo5lw$6Tf#aZ&l{|XLuwSU1;PZBYnW(Zh`dxvu+mVD*9Y;OM0BUVU z>D!YhZ(u3K3MrSpOR|TjESP8`N46GOPima{4+=|NebCh#tRts`G2|xp5)YbuR-7U# zbkrWtfn}@ldL;@ngKakHf+M7^U)p}vd=uo|8_djxx&JXpfi}2UPN0<&pXOmdANzzz zAQv`fG=9-2L)hC`ja1^RH;ftdvHUB%Bsq+>R8y`Tg?;*R1AQ&-#;T4kOd7GT5S$q? z)t{eJHW;n*`4T!`9$$D(vC%`xLQ;2v^g|7-T&GwU$bHLkC9o2w&fY$pI4si@F}ZC6?Ra$7YCqCpcGl`F%9=lGd6MWyoN{Glw-itR zIXOk5?v%?-1%rk?U*Mx@1F+E67(R!WD)?->sYd^R>Gc`)xibN8arzM;ok=gUHuq!U zZez&huo(Hz`E_0`0$MoP7(qam$Xw6yTaz6MF@LI}By$Cq{5&DPA}1@Q1^}R_Bdlpdb#=|n%{@Im!^6W96BG0E^Q)_? zySuxmr>96eXJ=^CQvQmWDu#*ky;1~ zEz;pxNn?5pXSZ#HzgtNXgvGb755V)O7Wm-m0jmQKfnI&9w^Fj$VVzYoRP#U%$!eE) zf!?vaMw!vi@0I2DFYicJcn?@Lf>#+W)r1QYPj&0SshrjlM;byJ=QC`z1bWgmzDD=rzo%ORq9fJkrw zN$x+a;BO6CO&zs)$Y#LuhUaAccyu>)gaMc%OY(SkS}>J5%O>k{VgYQ>J6?xfB_KaO? zjjhiKN00alr}#6*9=L@HX}VL5(~t7!=Qz2WEH`%|j;Gu*G?vk;*GY96p04pF@-o^t?Q`2(X3?~!e><-Z}|~?V97IX zQ@O+$YnKH71Z59-=z${yKYgY4k{J%n3t?7=1Eph`)m2~0s{`}Yi2mtqEIjuDAw&Kj z-u`p^?>htk$N1;N`QL~C-Eh8{`)fk}t+{_cd*|lk?uPA zLBknbbR1GQi7buyO4*YG7b&vW%0U7dd(0Dbs{4_26~kC!8&mJmqQB~LbY!i88Mgb` zEtWoV+Zi)5Mk2^6GSs|q{2uyP1hFk<@wHddm$#5d!XPE*n)r$%fQBJ1*VCQ5vd1nfhYyh^(cP(l zVwKUjjFo}Ar5mQh)e$A^jn3MwJs_YBqf z`dr92zUpVKizfhpb5iF*fXL>b8L1!bUuXgV=bY4qgd>|jBmEac^?R^$#^^$NkjpQ7ad0_B47 z`aQ~zWBT|Dl^~^qNS*KLp7TWOLX?pxfBOz3L-uVGf7AS) zT{x$ME+iZoFCmHAzsRAVwZ1PPod;SMqK)xA5An0!_o?D}dFet5khjJc#ipMXzYhz4 z9N-tcAM?s@n%~&xpTourlQKvL@Vg-L8_Kt7`rdn-`{fH^CHsw+R#Ct}me>G*>&TBO L(&?oAwN3s9fTSbz literal 0 HcmV?d00001 diff --git a/tests/gcs/files/helloWorld.xls b/tests/gcs/files/helloWorld.xls new file mode 100644 index 0000000000000000000000000000000000000000..2aca8c01bfba61f36abd88f17eb1a3388a722ef5 GIT binary patch literal 5632 zcmeHLU1(fY5T3g~>D~Np(xhr?<4w?Qel}w4gB7XSCatz$ZAc>(Dd;A-NnF0k}?n90Bz>J!y-P=8r(u2 zSa5CS8V0b4K_J(B9gw_`Ti-%cS~GP89_W`Oev@L!PMJWRk{7VzrNah9j8|PLJ-zv& z6#3<3*7PrVU^!p0-RpnmZ{~47Fn|9u*K_}u11o@)z$#!hum*@DKx%z&n7{x6!RhH-Wy({cd*ar5UVIH`43F2+cf$h6ccMJD&9!07%{c|q?RpO zR--iJQ|(^A&%mb^r{_Y>&mE2*GWqANEvQvaG{tzt*+sGOg><0j<}k*J)j!4rZUx*gb*xf02nAJ9Xbd+n(%t9edM< z-<)9rMFvh9@~b?>44vuM>6B%diMfRt2aG8%Zjp)%Gx(Rd+naLCK*l#`v4g~nNz-NK zc;tK&x!*)iH~&Q1zlQ9d8v*=Xb|R6q_9s(`;pQw33sYiH#lvFAYyg6AfrkUZ#}mcL zJmJ4(o=R9a49mG!Yt`B?hP!t5^7nfN`?{Z2ej1z{+(RE?I>*p57iiZW9a>JSY`xqq z%_5QBNXx+3xP8QW+C&hyiq=QG`H9{P;5;7A?D^!S^Ws1Y@C{d2V;Phf0QUREE9@md8B zTY!%Tud{egY5KT4N^?Cx)X0J!*UkYg@|*P0ota%|gex<2or=P5HR8|QhwmB*WLlLU z$~fcU!vj7k*M1BjJ`W!D)yXINE{u7kHgP;tonpXvP`*(_%;?f$q0QjY#!d_X1p~JO zKfc%yHW{DD_gcy$yff?>8%iaQBu6H#ozD;13G2ZJItSu|h&Wr1s-Cft5hv|CeO6q; z8IIBw_jEceVmd~FA?W1!6YuA+HY6#&|<+(j0O_VNc^xO<;`KUT7Z29@`5Wp7ltS zMp8fL!~8`krB@?_cDsTG^d`hErCULna_Dz7J7qzkJm#JF=39*rlzlRV`H$n)%eVh- z{0lImYeLQ>Zo^4%ibJcbp9e#!^WOy7_uL_avI821)CODsfmoS6TmQlQE$E(f^{+;? NQvZA3-~YD$zW}r$Fiije literal 0 HcmV?d00001 diff --git a/tests/gcs/files/helloWorld.xlsx b/tests/gcs/files/helloWorld.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0c12530983668fc9ebdfb4010cef5a0d0d2c4479 GIT binary patch literal 4767 zcmaJ_2Q*yk+9n92PNIumf*^!M&mkeA4MUV5dS|p5i7 zgNQm>_-D?!|K;Z1bDq6s?>%en_xsjcpZER1DtP!*I7CE5IN+3R9h^%b!MqzoEg>+$ z+t@oY=BqN^9bm|oSJ)f4TP3x;MOl4le~O})Z&J^hsaUcQ{c|sP0Fi!vE@70C_inG4 z9Af6W)qYs&<1huM0)4*l$WuCab~-tF{YJgLZZAiljxr~Oc~`-33uKQq#u4}94WaR8 z8ck1)B)^&DGFFFJ%vit!9Sn`}&Mj6CLh}!1ZDvm*bVAU9i6!~#3L$PwMpqOh2EOTL zy%9k3C=NvqN-3r}izE?pf0O_QPg9$0h+ zlc*w6h&ro_0j+i0j5H2#+sNR6RS1c!3j9CKW7L+qf`j@0cZQ+BeDM;qhZEG^%*o0A z_Dcsy#FI|PDIs7znh$^0LBPoFMgpF0{DiBNkG_ z1on`KK8kxh8?vN|ku9@2S*4H){-D6TBDN^8UTY)D&A{%|qYt{1#u16zo<263U6Fd7 zxde9QgL#4zZXMB*-!z4b=ZO}i>Cn7BvNk!Yx8(Xpw>jr6eA*3$*k)}xjLIM0{!~4n z*+$yCpdS&M)p1TeQ5oQ|l)Rbt-8z%lD~ukwy0hX99kbRjc|?*LX|*`(MrfvM+GDjK zZN|K_k;$aha})PIUr_04m^hVM8lxYog67^*!&=8?q0If|t0<#j0d2`Gl7bCae=130 zK4GQs-iP4EY1(MH4b%5CsVjm&(q-=BjL?VUP+_q5Ai;w|z$Sx_Vc*ECue=&($OB+y ziE~>N&5XNt%&_aZ3P{u9s^l+m-aA_+@;%;l=?X&@q7hdIBpJz?e!dVs6k<@ny5@iw zoR0wstNX+hGNSywl^)M!WjZb4c5Mdfz<+R7zKHYeLY*;*H0ZXVXt6Q#`lZMw8W#u0 z5$~U3hU~AHfw{UvEMeG)*?g+5J|)ChzxCwl=O|7ts-!{Iwqe^uKmPQ^8Ehx`s9NO9tdR#O9zld3O|yekd=SEc3-{QAP&b1xWhc z<&B$OZ5hY2HG4b2vA}x02C9U=zo^n%_wWJ!Vgw;KGKc$zECo7vR`fPin>$MvKAaU~ zXC^QLCC3}yrv;I9DnGRo92)(VTQi*V>Lxed!>PtPwDR|oir{V(TbzjIEu}+;`^^Lk z!QYsVv)(Q-s8XttkTfCU-~DL>0zVlt;Rk2<Fo}tpG%)9!!$sWny3pq)ya7Mg&&Q=g-^_6v# zreveC;^dY=6sS9a z*E@ETF9Gj4n7%NKRw*bYHOml&^88F{M??{;?-YPGw}J}o1}CRAAOn*B|1#`pX+POh zp??ftzB$V$7V8Ctou&8UH!s%8^icF|4-~Z@YL{+$u6}1hHUx-x^*@rRV%;xQ+s1kH zqd=m0ZYC{-BBs_@*fDu8+a$#~H;{Gxc-2B)+&k9SJGjAP&{ZDsSy|r>WI4K*I&_Wu zjMDn<*hHw?F~b%DMLL?1mm0-$hN=ZnUQKCuJUEAq-f;C`ssK#2Gr#sv(Mxwx@nANV zmaZ_tKOc9nft;po=tL<6^!lz_c;J_(A_~aKEz#4;74y|X6!5ZcuM}FB>C`iu!zm|? zecET$C;7BA@sVVxzpiMK3gb3or?KMMw&>@oE;lv4 z2!AHL;nQK1^Z2e?CBE}DT%Ou+mG67CoTcsn>N3jVjE*N6`AON?6WlsCOr)g?EQHh5 z)MLT`)M7+iJusmX9(i)wq%mrqF<-hjJ=eSph#jQLjZgJnPM2`xPHxj{)Y(YipWW!C ztW#dPDnR2HV?Lq@M=a|>4-DRFpD~(Ii?~&IKWaq8OaXYOz2@nxZE+ezHP~|Sb_pa# za!m%W9NnF2rkU1dI{iUY+}U3p%MY4t*XtrsTJ2`0=SkQ;>ZuJtr}$rUh1mZ9xvrs4ZO#YzMVdFhX4edYt#!{zu0ib<6 z?d-DhV{?u@E=JkGU$QokY$A|%rxU*ZAGAHC{P$jlzo#);o0Kd>sCb34dGm@Bjz}S< zd|0W*!TX#;qv*$D+tn1``Rtp?JRqGO9q(+RtnVa#UC)bz$!XrmoK^@yUW*^jzq=-9 zy%kdA4apop9T&=TFoPv->XwH!lBS;i@toZUrHD8>ZR#-so6vjsNJ@CYYitP3#5KDE zz3P(E4`gyC$(cI$eBKI^PUC=CYX&6N+~kn?eD{)&FEd7SZkA^Qh@`UZvk%76AV&Al z2!M+?^N0h$PHsXoH$20lnc1j~AwonI+_1&jz12s3ZbUp*oEcjA!v+OIol)y}XBx;< z5qmx}PhIka|VXzWMH=<};hpaN`sC8V1q?#)_Z!$X*v|*fZS?tKuOph=t<% zWK1NBrJlp-JnpGP?^-oj3hL#~6*tW#%GkSZIoTp7SHmh-$aGYH!SLYvDeqi@;brcJ z;_9)uZ5*N&XXFF+*Mqjka4TDtV_n|5Zk#(&NB-y-sh|?7R(ER0+%V4|SB|zO+h$`enP~ zdbiD*-bfkdbSkUmk9F1rY-$L0?f{U)G`1PAfe;9Ut#kLv$YDF0bWB2imdHBz-51DSGy` zE|P{~ovUk6i1}z?IO^THO-xHHXKH=k;n+!A$!oqeu?_mFB)?1q-@4;?w9Iw#M0pyi zPd^!R8mjEM7!K-=;O@gTOeRAW z6JDNIDrOY%oJ*H018>iS_?(%akzxIlM18zvis==n{xJ@Lm;Gbv?EELoz5zKkVe|5Y zTDzqJ#A6TwW+=Cv1lUTs+I+gD@~k6Hkc-#frvZn-xsCgzVt?Fw4eI1t8UTzh6F}(T zQ=0_Eb~)qwAFRGB2UiXvwOYfE+YNkX}uqC zN^``HY1rzGQq46*F6~d)iFy?%Mh-PLC~S{t9}r^AR+;06IKmhOfid0RhczYU3j;YP zM^{TnS7R-AXG@qN)^hb)QtHh@z}XW%_F|qF-M&EONxycYcpeeqZLT{JEFWhE&)|K$ zTG{~K(9q70Tb~ovUx-elD+ zaFv_9V#fTlCXMP%Pc;epnJFGTQi+a9Ts?8{ovBlsfN{9q;Fv^|5c!Vm%bbs*vz~@3Hkrb WQ?LpF#>zN20L&$WsgKHk?)iUfObnU; literal 0 HcmV?d00001 diff --git a/tests/gcs/files/helloWorld.xml b/tests/gcs/files/helloWorld.xml new file mode 100644 index 0000000..e9e2f4d --- /dev/null +++ b/tests/gcs/files/helloWorld.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/tests/i18n/lng-from-cookie.spec.ts b/tests/i18n/lng-from-cookie.spec.ts index 91586a5..34176d9 100644 --- a/tests/i18n/lng-from-cookie.spec.ts +++ b/tests/i18n/lng-from-cookie.spec.ts @@ -1,5 +1,5 @@ import { test, expect, chromium, BrowserContext, Page } from "@playwright/test"; -import { fallbackLng } from "../../app/i18n/settings"; +import { fallbackLng } from "../../../app/i18n/settings"; import { lngs, siteUrl } from "./testUtils"; const cookieName = "i18next"; @@ -134,4 +134,4 @@ test.describe("Server Language Response Tests", () => { await expect(page).toHaveURL(/.*en-GB/); await browser.close(); }); -}); \ No newline at end of file +}); diff --git a/tests/i18n/lng-from-default.spec.ts b/tests/i18n/lng-from-default.spec.ts index 732e67d..30c99d6 100644 --- a/tests/i18n/lng-from-default.spec.ts +++ b/tests/i18n/lng-from-default.spec.ts @@ -57,4 +57,4 @@ test.describe("Default Language Redirection", () => { await page.goto(`${siteUrl}/fr-CA`); await expect(page).toHaveURL(`${siteUrl}/fr-CA`); }); -}); \ No newline at end of file +}); diff --git a/tests/i18n/lng-from-url-prefix.spec.ts b/tests/i18n/lng-from-url-prefix.spec.ts index 74ce8b5..65c41eb 100644 --- a/tests/i18n/lng-from-url-prefix.spec.ts +++ b/tests/i18n/lng-from-url-prefix.spec.ts @@ -1,5 +1,12 @@ -import { test, expect, chromium, Browser, BrowserContext, Page } from "@playwright/test"; -import { fallbackLng } from "../../app/i18n/settings"; +import { + test, + expect, + chromium, + Browser, + BrowserContext, + Page, +} from "@playwright/test"; +import { fallbackLng } from "../../../app/i18n/settings"; import { EN_WELCOME_MSG, FR_WELCOME_MSG, @@ -21,13 +28,15 @@ test.describe("URL Prefix Language Redirection", () => { }); test("should redirect to the i18next default language when Accept-Language is not available or unsupported", async () => { - const { page } = await createPageWithAcceptLanguage('unsupported-locale'); + const { page } = await createPageWithAcceptLanguage("unsupported-locale"); await page.goto(siteUrl); expect(page.url()).toContain(`${siteUrl}/${fallbackLng}`); }); for (const url of enUrls) { - test(`should contain the correct message for English at ${url}`, async ({ page }) => { + test(`should contain the correct message for English at ${url}`, async ({ + page, + }) => { await page.goto(url); const pageContent = await page.textContent("body"); expect(pageContent).toContain(EN_WELCOME_MSG); @@ -35,7 +44,9 @@ test.describe("URL Prefix Language Redirection", () => { } for (const url of frUrls) { - test(`should contain the correct message for French at ${url}`, async ({ page }) => { + test(`should contain the correct message for French at ${url}`, async ({ + page, + }) => { await page.goto(url); const pageContent = await page.textContent("body"); expect(pageContent).toContain(FR_WELCOME_MSG); @@ -43,14 +54,14 @@ test.describe("URL Prefix Language Redirection", () => { } test(`Unsupported language prefix defaults to Accept-Language header value`, async () => { - const { page } = await createPageWithAcceptLanguage('fr-CA'); + const { page } = await createPageWithAcceptLanguage("fr-CA"); await page.goto(`${siteUrl}/unsupported-lng/`); await expect(page).toHaveURL(/.*fr-CA/); }); test(`Unsupported language prefix defaults to i18next default language`, async () => { - const { page } = await createPageWithAcceptLanguage('unsupported-locale'); + const { page } = await createPageWithAcceptLanguage("unsupported-locale"); await page.goto(`${siteUrl}/unsupported-lng/`); expect(page.url()).toContain(`${siteUrl}/${fallbackLng}`); }); -}); \ No newline at end of file +}); diff --git a/tests/i18n/testUtils.ts b/tests/i18n/testUtils.ts index c93139f..54d4404 100644 --- a/tests/i18n/testUtils.ts +++ b/tests/i18n/testUtils.ts @@ -9,13 +9,13 @@ export const enLngs = ['en', 'en-CA', 'en-GB', 'en-US', ]; export const frLngs = ['fr', 'fr-CA']; export const lngs = enLngs.concat(frLngs); -export const enUrls = [ +export const enUrls = [ siteUrl + "/en/", siteUrl + "/en-CA/", siteUrl + "/en-GB/", siteUrl + "/en-US/" ] -export const frUrls = [ +export const frUrls = [ siteUrl + "/fr/", siteUrl + "/fr-CA/" ] @@ -26,5 +26,3 @@ export async function createPageWithAcceptLanguage(locale: string = ''): Promise const page = await context.newPage(); return { browser, context, page }; } - - From ad28ac5704ed08634a698911a47dff877d05018b Mon Sep 17 00:00:00 2001 From: shon-button Date: Tue, 18 Jul 2023 09:32:46 -0400 Subject: [PATCH 02/14] modified package.json\scripts\dev to export GOOGLE_APPLICATION_CREDENTIALS required for Google Client Libraries. You will need to set .env.development\GAC_EXPORT value to the value of the service account key location (https://console.cloud.google.com/iam-admin/serviceaccounts/details/106707473171516793046?project=emissions-elt-demo) --- README.md | 5 +---- credentials/service-account-key-gcs.json | 13 ------------- package.json | 6 +++--- 3 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 credentials/service-account-key-gcs.json diff --git a/README.md b/README.md index cc22b57..129915b 100644 --- a/README.md +++ b/README.md @@ -218,10 +218,7 @@ Linux/Mac: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json ``` -**Note**: Storing a service account key file fro GCS within the file directory credentials/service-account-key-gcs.json, while not considered a best practice, has been done for convenience and ease of team collaboration, local GCP auth setup, and testing by using scripts to export the service account from the credential folder. -**Example**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server - -While this approach does provide convenience during local development and testing, it's important to handle sensitive credentials securely to protect against unauthorized access or exposure by using proper file permissions and preventing commit to GitHUb via the .gitignore file. +**Note**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server. You will need to set .env.development\GAC_EXPORT value to the value of the service account key location. Example: GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/credentials/service-account-key-gcs.json" To echo the value of the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: diff --git a/credentials/service-account-key-gcs.json b/credentials/service-account-key-gcs.json deleted file mode 100644 index ad6a115..0000000 --- a/credentials/service-account-key-gcs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "emissions-elt-demo", - "private_key_id": "ecc9c7e27bf42d989143745aea253a5f93e49aac", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT0/94jFq0fiwV\n9/FTyzc7SZ498sy3ZNJrXa0DRlnAP9YccbTBTCaa44So/xTrlEm5lP9XPd4CmKmD\nunRXGFo0L0wmyniO6HIeldclDMgzwOu4ptlreyL70Fc81hd2hre7Zu02xys2I4sw\nImieJ/tuWsRscYUgUeOLkIbPkG/UM42/XhYf9Kjx8cucQcui30WRwRxwbK3Mr/wr\nue9xyDgIFrE7ZBEiHA7OSt/Ae70FKqlAtMlWgR2LdXGts0Q1iA3Qy2kwPJMMPP5Y\n7KZJAq5wqRnx7WFNHqrAzkxcdYpVKVx+Q1loUJ9qM2lLhIKUS3mk7riO2PTOv0Fj\nfbz1fsXHAgMBAAECggEAMZSGWAuW9ndk2N9mUMjVFuzrhnJzJ8VIb5slBnanbnva\nl4KpcbVVM1jAqx+WiCadjYFEHKIS3oMOQ7CbCYUQ5/S/ETmSMrgSYmC2HmaJlRYM\n2UsYm9xaUOPBBpX1m5q2b8OnJtqpCwjjy3qW5Qia4xnNTGPMlxjv/OS12lLituQr\nFaaKnzo1cpGDSmtkIlyANV7NEfbe4b08fetPQOOUDMnQ4Cal78m8pURt5Ineps9a\nZ8sTPqU58fEe4oD7q/zbVF5DVltwq3xg/gjQAWwgOIve9E6TkCjjJH0Fli6miOi8\nWtHxu8MUpuHa6VpQGCvEs6jJtlPPMdr50E8N+37jFQKBgQDDMA4SBTQodYqQWi7x\nKQqkcnnALI2ZZVKUJlUzz+G0dL8d9zuYhYvDGUCFP2zd+TmID7RV3C4tJ4Rqt30q\n/Eg0WYCp+S5rCrmEl4q547Q54U3+zUR9IFgBsz2i8u7t0JX0B4QTCW9mQvlJA4pB\n+a1Hm+4/Khw6mn4NwylyakZrBQKBgQDB4pi3oRS8Cb34yRF6fhvycllAGy22z0lw\n88e6Tub8gq7dgiiw/+/ehPrBuqXczvK6gzRNO72oFHmz/UPSdW1xS58kauQ3W2tr\nbNdrqYOFkuO7ucQd8JGtEpI9MFZ8GT+Z672P7EFYKDqExyscXw7y0VqmcQkuiP52\n1IaCIIa/WwKBgQCLKQbPGECwm+T3uCSBsfYxeqCNP/aQqCmxEIdskkjkRNxBvBQU\nURptNeLHXYn71IWNGU1Ebd/KN8Nz5nBqJkZAdJOEG/FZReMwwm6Yy9yh652VDbpH\nz7iPNcC7HaL1kOJogrdKb06qRRPAV7LKCP3e8TByfk50BdPbcgpp1ZVxFQKBgEga\nMJj5enCDXvaKL8nR5CrBg5dnhBSb+b/bqMcMWLJHFyihIujQBTBHW8l30/7Np07d\nRDIEqX88PhZFdVdq/AxKByDP75b2lHgavfH31EV0XuSNLPXFZSdr5J6Ev2TfLtva\n42AGiDZ0n26JcurWHwUF/iQvnS6FG7ytRGhYGERJAoGAHxYwRdfjRCmdnbNvm1j3\n8KSkDWN6W2tEzgSJAQMf36G/ttbIdM93T/XY5xqfeC8cGZY732uFSkYkrRPboCA9\n9pMwctxacOr4uCsiFEV6PhFWu+hp9hMyHlgakrW4NVXb6GoU+DjruBX3ktLXBNe1\nJ+VrC317w6x8UdzvHHrEJ5g=\n-----END PRIVATE KEY-----\n", - "client_email": "cloud-storage-sa@emissions-elt-demo.iam.gserviceaccount.com", - "client_id": "106707473171516793046", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/cloud-storage-sa%40emissions-elt-demo.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/package.json b/package.json index fa41f0a..e7c583f 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,14 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && next dev", + "dev": "echo $GAC_EXPORT && next dev", "build": "next build", "start": "next start", "lint": "next lint", - "test:codegen": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && npx playwright codegen", + "test:codegen": "echo $GAC_EXPORT && npx playwright codegen", "test:e2e": "playwright test", "test:i18n": "playwright test tests/i18n", - "test:gcs": "export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json && playwright test tests/gcs", + "test:gcs": "echo $GAC_EXPORT && playwright test tests/gcs", "k8s": "minikube start && scripts/k8s-secrets.sh && scripts/k8s-launch.sh", "tailwind:watch": "postcss tailwind.css -o styles.css -w" }, From afd6057e83f2192ed495892c1f41e64fe20da547 Mon Sep 17 00:00:00 2001 From: shon-button Date: Fri, 21 Jul 2023 11:20:59 -0400 Subject: [PATCH 03/14] =?UTF-8?q?This=20commit=20includes=20the=20followin?= =?UTF-8?q?g=20changes:=20=F0=9F=86=95=20storing=20service-account-key.jso?= =?UTF-8?q?n=20as=20a=20string=20in=20scripts\tests\.env=20=F0=9F=86=95=20?= =?UTF-8?q?created=20scripts/tests/test-gcs.sh=20to=20set=20the=20export?= =?UTF-8?q?=20GOOGLE=5FAPPLICATION=5FCREDENTIALS=20from=20the=20.env=20val?= =?UTF-8?q?ue=20for=20the=20Playwright=20GCS=20tests=20=F0=9F=94=A7=20chan?= =?UTF-8?q?ged=20package.json\scripts\dev:=20scripts/tests/test-gcs.sh=20?= =?UTF-8?q?=F0=9F=93=9A=20Updated=20READ.ME=20with=20scripts/tests/test-gc?= =?UTF-8?q?s.sh=20dependancy=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 5 +- .env | 4 +- .github/workflows/playwright.yml | 9 +++ .gitignore | 3 + README.md | 2 +- app/logs/errors/file.json | 100 +++++++++++++++++++++++++++++++ package.json | 8 +-- playwright.config.ts | 9 --- playwright/.auth/user.json | 4 +- scripts/tests/.env | 2 + scripts/tests/test-gcs.sh | 35 +++++++++++ tests/.env | 3 +- 12 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 scripts/tests/.env create mode 100755 scripts/tests/test-gcs.sh diff --git a/.dockerignore b/.dockerignore index 310601f..db6d24f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,8 +33,8 @@ yarn-error.log* # local env files -.env*.local -.env.* +.env._ +.env_ # vercel @@ -54,5 +54,4 @@ keycloak-sa-credential.json # gcp -/credentials/service-account-key.json \nplaywright/.auth diff --git a/.env b/.env index 91f3c01..6b42d18 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ + API_HOST=http://localhost:3000/ DATABASE=eed DATABASE_HOST=34.125.92.26 @@ -18,6 +19,7 @@ DATABASE_USER_PW_MANAGER=secret DATABASE_INSTANCE_CONNECTION_NAME=emissions-elt-demo:us-west4:eed DB_USERNAME=keycloak_actor DB_PASSWORD=keycloak_pass +GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/home/shon/Workspace/Button/credentials/service-account-key-gcs.json" GITHUB_ID=291382f53280c6de8f4d GITHUB_SECRET=aaa22549c1066fbf1138505fa03a28e03ab045b8 GOOGLE_BUCKET_NAME=eed_upload_file_storage @@ -25,4 +27,4 @@ GOOGLE_CLIENT_ID=77682378143-061racvi899q8vvgmcioqhbnu8pokm9o.apps.googleusercon GOOGLE_CLIENT_SECRET=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 GOOGLE_PROJECT_NAME="emissions-elt-demo" NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk +NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk \ No newline at end of file diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 1a479d1..3958026 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,11 +1,20 @@ +# Name of the GitHub Actions workflow name: Playwright Tests +# Trigger the workflow on 'push' event on: [push] +# Concurrency options for the workflow concurrency: + # Define the concurrency group for the workflow with placeholders group: CT-caller-${{ github.workflow }}-${{ github.ref }} + # If a workflow with the same concurrency group is already in progress, cancel it cancel-in-progress: true +# Define the jobs that will be executed as part of the workflow jobs: + # Job to call and execute the 'test-code-playwright.yml' workflow call-workflow-playwright-test: + # Use a reusable GitHub Actions workflow from the 'button-inc/gh-actions' repository + # and the file 'test-code-playwright.yml' in the 'develop' branch uses: button-inc/gh-actions/.github/workflows/test-code-playwright.yml@develop diff --git a/.gitignore b/.gitignore index 5c51df1..e25bb66 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ next-env.d.ts # keycloak keycloak-sa-credential.json emissions-elt-demo-ecc9c7e27bf4.json + +# playwright +/tests/.env \ No newline at end of file diff --git a/README.md b/README.md index 129915b..d4ed0db 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ Linux/Mac: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json ``` -**Note**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server. You will need to set .env.development\GAC_EXPORT value to the value of the service account key location. Example: GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/credentials/service-account-key-gcs.json" +**Note**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server. You will need to set .env.development\GAC_EXPORT value to the value of the [service account](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106707473171516793046?project=emissions-elt-demo) key location. Example: GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/credentials/service-account-key-gcs.json" To echo the value of the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: diff --git a/app/logs/errors/file.json b/app/logs/errors/file.json index b0d0588..8bd8647 100644 --- a/app/logs/errors/file.json +++ b/app/logs/errors/file.json @@ -46,3 +46,103 @@ {"timestamp":"2023-07-17T20:24:31.261Z","type":"upload","error":"unsupported"} {"timestamp":"2023-07-17T20:58:08.669Z","type":"upload","error":"Failed to upload file. FetchError: request to https://storage.googleapis.com/upload/storage/v1/b/eed_upload_file_storage/o?name=helloWorld.xlsx&uploadType=resumable failed, reason: "} {"timestamp":"2023-07-17T20:58:41.189Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:04:12.695Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:04:39.195Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:05:09.749Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:05:40.480Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:06:11.398Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:10:07.161Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:11:35.482Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:12:03.253Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-19T16:13:13.025Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:15:00.198Z","type":"upload","error":"Failed to upload file. Error: The file at echo does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/home/shon/Workspace/Button/climatetrax-frontend/echo'"} +{"timestamp":"2023-07-19T16:15:28.636Z","type":"upload","error":"Failed to upload file. Error: The file at echo does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/home/shon/Workspace/Button/climatetrax-frontend/echo'"} +{"timestamp":"2023-07-19T16:15:53.020Z","type":"upload","error":"Failed to upload file. Error: The file at /credentials/service-account-key-gcs.json does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/credentials'"} +{"timestamp":"2023-07-19T16:17:45.769Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:23:54.093Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:26:13.259Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-19T16:39:40.283Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-21T13:34:07.826Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:34:37.103Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:35:08.236Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:35:39.259Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:36:10.184Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:36:40.800Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:37:11.784Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:37:42.783Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:38:13.680Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:38:44.585Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:39:15.287Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:39:46.248Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:40:17.084Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:40:48.098Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:41:18.983Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:41:50.048Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:42:20.782Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:42:51.888Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:43:22.519Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:43:53.650Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:44:24.470Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:44:55.294Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:45:26.327Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:45:57.248Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:46:28.065Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:46:58.994Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:47:29.709Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:48:00.842Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:48:31.480Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:49:02.812Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T13:49:33.166Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-21T14:00:22.334Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:00:52.995Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:01:24.171Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:01:54.685Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:03:40.481Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:04:10.387Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:04:41.509Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:05:12.324Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:05:43.562Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:06:14.245Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:06:45.099Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:07:15.828Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:07:46.881Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:08:17.583Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:08:48.698Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:09:19.419Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:09:50.342Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:10:21.394Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:10:52.287Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:11:23.116Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:11:54.093Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:12:25.191Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:12:55.889Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:13:26.582Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:13:57.851Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:14:28.590Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:14:59.482Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:16:30.835Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:22:29.751Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:22:59.953Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:23:31.084Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:24:01.801Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:24:32.683Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:25:03.550Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:25:34.374Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:26:05.399Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:26:36.120Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:27:07.187Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:27:38.332Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:28:08.831Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:28:39.936Z","type":"upload","error":"Failed to upload file. Error: Shon.Hogan@gmail.com does not have storage.objects.create access to the Google Cloud Storage object. Permission 'storage.objects.create' denied on resource (or it may not exist)."} +{"timestamp":"2023-07-21T14:32:57.516Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:33:28.340Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:33:59.237Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:34:30.341Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:35:01.240Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:35:31.971Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:36:02.803Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:36:33.898Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:37:04.794Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:37:35.447Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:48:15.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-21T14:49:56.922Z","type":"upload","error":"unsupported"} diff --git a/package.json b/package.json index e7c583f..0a928d3 100644 --- a/package.json +++ b/package.json @@ -3,15 +3,15 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "echo $GAC_EXPORT && next dev", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", - "test:codegen": "echo $GAC_EXPORT && npx playwright codegen", + "test:codegen": "npx playwright codegen", "test:e2e": "playwright test", "test:i18n": "playwright test tests/i18n", - "test:gcs": "echo $GAC_EXPORT && playwright test tests/gcs", - "k8s": "minikube start && scripts/k8s-secrets.sh && scripts/k8s-launch.sh", + "test:gcs": "scripts/tests/test-gcs.sh", + "k8s": "minikube start && scripts/k8s-secrets.sh", "tailwind:watch": "postcss tailwind.css -o styles.css -w" }, "dependencies": { diff --git a/playwright.config.ts b/playwright.config.ts index 945eb56..be4d854 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,15 +18,6 @@ const config: PlaywrightTestConfig = { // Artifacts folder where screenshots, videos, and traces are stored. outputDir: "test-results/", - // Run your local dev server before starting the tests: - // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests - webServer: { - command: "npm run dev", - port: +PORT, - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, - use: { // Use baseURL so to make navigations relative. // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url diff --git a/playwright/.auth/user.json b/playwright/.auth/user.json index 21030a8..6595aac 100644 --- a/playwright/.auth/user.json +++ b/playwright/.auth/user.json @@ -15,11 +15,11 @@ "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ", "domain": "localhost", "path": "/", - "expires": 1689713869, + "expires": 1690037382, "httpOnly": true, "secure": false, "sameSite": "Lax" } ], "origins": [] -} +} \ No newline at end of file diff --git a/scripts/tests/.env b/scripts/tests/.env new file mode 100644 index 0000000..559722b --- /dev/null +++ b/scripts/tests/.env @@ -0,0 +1,2 @@ +SERVICE_ACCOUNT_JSON='{"type":"service_account","project_id":"emissions-elt-demo","private_key_id":"ecc9c7e27bf42d989143745aea253a5f93e49aac","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT0/94jFq0fiwV\n9/FTyzc7SZ498sy3ZNJrXa0DRlnAP9YccbTBTCaa44So/xTrlEm5lP9XPd4CmKmD\nunRXGFo0L0wmyniO6HIeldclDMgzwOu4ptlreyL70Fc81hd2hre7Zu02xys2I4sw\nImieJ/tuWsRscYUgUeOLkIbPkG/UM42/XhYf9Kjx8cucQcui30WRwRxwbK3Mr/wr\nue9xyDgIFrE7ZBEiHA7OSt/Ae70FKqlAtMlWgR2LdXGts0Q1iA3Qy2kwPJMMPP5Y\n7KZJAq5wqRnx7WFNHqrAzkxcdYpVKVx+Q1loUJ9qM2lLhIKUS3mk7riO2PTOv0Fj\nfbz1fsXHAgMBAAECggEAMZSGWAuW9ndk2N9mUMjVFuzrhnJzJ8VIb5slBnanbnva\nl4KpcbVVM1jAqx+WiCadjYFEHKIS3oMOQ7CbCYUQ5/S/ETmSMrgSYmC2HmaJlRYM\n2UsYm9xaUOPBBpX1m5q2b8OnJtqpCwjjy3qW5Qia4xnNTGPMlxjv/OS12lLituQr\nFaaKnzo1cpGDSmtkIlyANV7NEfbe4b08fetPQOOUDMnQ4Cal78m8pURt5Ineps9a\nZ8sTPqU58fEe4oD7q/zbVF5DVltwq3xg/gjQAWwgOIve9E6TkCjjJH0Fli6miOi8\nWtHxu8MUpuHa6VpQGCvEs6jJtlPPMdr50E8N+37jFQKBgQDDMA4SBTQodYqQWi7x\nKQqkcnnALI2ZZVKUJlUzz+G0dL8d9zuYhYvDGUCFP2zd+TmID7RV3C4tJ4Rqt30q\n/Eg0WYCp+S5rCrmEl4q547Q54U3+zUR9IFgBsz2i8u7t0JX0B4QTCW9mQvlJA4pB\n+a1Hm+4/Khw6mn4NwylyakZrBQKBgQDB4pi3oRS8Cb34yRF6fhvycllAGy22z0lw\n88e6Tub8gq7dgiiw/+/ehPrBuqXczvK6gzRNO72oFHmz/UPSdW1xS58kauQ3W2tr\nbNdrqYOFkuO7ucQd8JGtEpI9MFZ8GT+Z672P7EFYKDqExyscXw7y0VqmcQkuiP52\n1IaCIIa/WwKBgQCLKQbPGECwm+T3uCSBsfYxeqCNP/aQqCmxEIdskkjkRNxBvBQU\nURptNeLHXYn71IWNGU1Ebd/KN8Nz5nBqJkZAdJOEG/FZReMwwm6Yy9yh652VDbpH\nz7iPNcC7HaL1kOJogrdKb06qRRPAV7LKCP3e8TByfk50BdPbcgpp1ZVxFQKBgEga\nMJj5enCDXvaKL8nR5CrBg5dnhBSb+b/bqMcMWLJHFyihIujQBTBHW8l30/7Np07d\nRDIEqX88PhZFdVdq/AxKByDP75b2lHgavfH31EV0XuSNLPXFZSdr5J6Ev2TfLtva\n42AGiDZ0n26JcurWHwUF/iQvnS6FG7ytRGhYGERJAoGAHxYwRdfjRCmdnbNvm1j3\n8KSkDWN6W2tEzgSJAQMf36G/ttbIdM93T/XY5xqfeC8cGZY732uFSkYkrRPboCA9\n9pMwctxacOr4uCsiFEV6PhFWu+hp9hMyHlgakrW4NVXb6GoU+DjruBX3ktLXBNe1\nJ+VrC317w6x8UdzvHHrEJ5g=\n-----END PRIVATE KEY-----\n","client_email":"cloud-storage-sa@emissions-elt-demo.iam.gserviceaccount.com","client_id":"106707473171516793046","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/cloud-storage-sa%40emissions-elt-demo.iam.gserviceaccount.com","universe_domain":"googleapis.com"}' +SERVICE_ACCOUNT_JSON_BASE64=ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiZW1pc3Npb25zLWVsdC1kZW1vIiwKICAicHJpdmF0ZV9rZXlfaWQiOiAiZWNjOWM3ZTI3YmY0MmQ5ODkxNDM3NDVhZWEyNTNhNWY5M2U0OWFhYyIsCiAgInByaXZhdGVfa2V5IjogIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuTUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFDVDAvOTRqRnEwZml3VlxuOS9GVHl6YzdTWjQ5OHN5M1pOSnJYYTBEUmxuQVA5WWNjYlRCVENhYTQ0U28veFRybEVtNWxQOVhQZDRDbUttRFxudW5SWEdGbzBMMHdteW5pTzZISWVsZGNsRE1nendPdTRwdGxyZXlMNzBGYzgxaGQyaHJlN1p1MDJ4eXMySTRzd1xuSW1pZUovdHVXc1JzY1lVZ1VlT0xrSWJQa0cvVU00Mi9YaFlmOUtqeDhjdWNRY3VpMzBXUndSeHdiSzNNci93clxudWU5eHlEZ0lGckU3WkJFaUhBN09TdC9BZTcwRktxbEF0TWxXZ1IyTGRYR3RzMFExaUEzUXkya3dQSk1NUFA1WVxuN0taSkFxNXdxUm54N1dGTkhxckF6a3hjZFlwVktWeCtRMWxvVUo5cU0ybExoSUtVUzNtazdyaU8yUFRPdjBGalxuZmJ6MWZzWEhBZ01CQUFFQ2dnRUFNWlNHV0F1VzluZGsyTjltVU1qVkZ1enJobkp6SjhWSWI1c2xCbmFuYm52YVxubDRLcGNiVlZNMWpBcXgrV2lDYWRqWUZFSEtJUzNvTU9RN0NiQ1lVUTUvUy9FVG1TTXJnU1ltQzJIbWFKbFJZTVxuMlVzWW05eGFVT1BCQnBYMW01cTJiOE9uSnRxcEN3amp5M3FXNVFpYTR4bk5UR1BNbHhqdi9PUzEybExpdHVRclxuRmFhS256bzFjcEdEU210a0lseUFOVjdORWZiZTRiMDhmZXRQUU9PVURNblE0Q2FsNzhtOHBVUnQ1SW5lcHM5YVxuWjhzVFBxVTU4ZkVlNG9EN3EvemJWRjVEVmx0d3EzeGcvZ2pRQVd3Z09JdmU5RTZUa0NqakpIMEZsaTZtaU9pOFxuV3RIeHU4TVVwdUhhNlZwUUdDdkVzNmpKdGxQUE1kcjUwRThOKzM3akZRS0JnUURETUE0U0JUUW9kWXFRV2k3eFxuS1Fxa2NubkFMSTJaWlZLVUpsVXp6K0cwZEw4ZDl6dVloWXZER1VDRlAyemQrVG1JRDdSVjNDNHRKNFJxdDMwcVxuL0VnMFdZQ3ArUzVyQ3JtRWw0cTU0N1E1NFUzK3pVUjlJRmdCc3oyaTh1N3QwSlgwQjRRVENXOW1RdmxKQTRwQlxuK2ExSG0rNC9LaHc2bW40Tnd5bHlha1pyQlFLQmdRREI0cGkzb1JTOENiMzR5UkY2Zmh2eWNsbEFHeTIyejBsd1xuODhlNlR1YjhncTdkZ2lpdy8rL2VoUHJCdXFYY3p2SzZnelJOTzcyb0ZIbXovVVBTZFcxeFM1OGthdVEzVzJ0clxuYk5kcnFZT0ZrdU83dWNRZDhKR3RFcEk5TUZaOEdUK1o2NzJQN0VGWUtEcUV4eXNjWHc3eTBWcW1jUWt1aVA1MlxuMUlhQ0lJYS9Xd0tCZ1FDTEtRYlBHRUN3bStUM3VDU0JzZll4ZXFDTlAvYVFxQ214RUlkc2tramtSTnhCdkJRVVxuVVJwdE5lTEhYWW43MUlXTkdVMUViZC9LTjhOejVuQnFKa1pBZEpPRUcvRlpSZU13d202WXk5eWg2NTJWRGJwSFxuejdpUE5jQzdIYUwxa09Kb2dyZEtiMDZxUlJQQVY3TEtDUDNlOFRCeWZrNTBCZFBiY2dwcDFaVnhGUUtCZ0VnYVxuTUpqNWVuQ0RYdmFLTDhuUjVDckJnNWRuaEJTYitiL2JxTWNNV0xKSEZ5aWhJdWpRQlRCSFc4bDMwLzdOcDA3ZFxuUkRJRXFYODhQaFpGZFZkcS9BeEtCeURQNzViMmxIZ2F2ZkgzMUVWMFh1U05MUFhGWlNkcjVKNkV2MlRmTHR2YVxuNDJBR2lEWjBuMjZKY3VyV0h3VUYvaVF2blM2Rkc3eXRSR2hZR0VSSkFvR0FIeFl3UmRmalJDbWRuYk52bTFqM1xuOEtTa0RXTjZXMnRFemdTSkFRTWYzNkcvdHRiSWRNOTNUL1hZNXhxZmVDOGNHWlk3MzJ1RlNrWWtyUlBib0NBOVxuOXBNd2N0eGFjT3I0dUNzaUZFVjZQaEZXdStocDloTXlIbGdha3JXNE5WWGI2R29VK0RqcnVCWDNrdExYQk5lMVxuSitWckMzMTd3Nng4VWR6dkhIckVKNWc9XG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4iLAogICJjbGllbnRfZW1haWwiOiAiY2xvdWQtc3RvcmFnZS1zYUBlbWlzc2lvbnMtZWx0LWRlbW8uaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTA2NzA3NDczMTcxNTE2NzkzMDQ2IiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9jbG91ZC1zdG9yYWdlLXNhJTQwZW1pc3Npb25zLWVsdC1kZW1vLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAidW5pdmVyc2VfZG9tYWluIjogImdvb2dsZWFwaXMuY29tIgp9 \ No newline at end of file diff --git a/scripts/tests/test-gcs.sh b/scripts/tests/test-gcs.sh new file mode 100755 index 0000000..568bec7 --- /dev/null +++ b/scripts/tests/test-gcs.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Get the absolute path to the script's directory +DIR=$(dirname "$0") + +# Load the variables from .env file +. "$DIR/.env" + +# Create a temporary file for the service account JSON +TEMP_FILE=$(mktemp) + +# Write the service account JSON to the temporary file +echo "$SERVICE_ACCOUNT_JSON" > "$TEMP_FILE" + +# Set the path to the temporary service account JSON file and export it +export GOOGLE_APPLICATION_CREDENTIALS="$TEMP_FILE" + +# Build the Next.js application +npm run build + +# Start the Next.js application in production mode +npm run start & + +# Wait for a few seconds to ensure the Next.js application is ready +sleep 5 + +# Run your Playwright test +npx playwright test tests/gcs + +# Clean up the temporary files and folder +rm -rf "$TEMP_DIR" + +# Find the process ID of the Next.js application and stop it +NEXT_JS_PID=$(ps aux | grep "npm run start" | grep -v grep | awk '{print $2}') +kill "$NEXT_JS_PID" diff --git a/tests/.env b/tests/.env index 2010c16..007171f 100644 --- a/tests/.env +++ b/tests/.env @@ -1,3 +1,4 @@ API_HOST=http://localhost:3000/ -NEXTAUTH_JWT_ANALYST= "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ" GOOGLE_BUCKET_NAME=eed_upload_file_storage +NEXTAUTH_JWT_ANALYST= "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ" +NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk From fb3d1fac652bea559df0f24d1a971a94f6d91ad2 Mon Sep 17 00:00:00 2001 From: shon-button Date: Mon, 24 Jul 2023 14:41:00 -0400 Subject: [PATCH 04/14] updated readme --- .github/workflows/playwright.yml | 11 +--------- .github/workflows/scan-code.yml | 36 ++++++++++++++++---------------- README.md | 4 ++-- app/logs/errors/file.json | 8 +++++++ playwright/.auth/user.json | 2 +- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 3958026..5da7e15 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,20 +1,11 @@ -# Name of the GitHub Actions workflow name: Playwright Tests -# Trigger the workflow on 'push' event on: [push] -# Concurrency options for the workflow concurrency: - # Define the concurrency group for the workflow with placeholders group: CT-caller-${{ github.workflow }}-${{ github.ref }} - # If a workflow with the same concurrency group is already in progress, cancel it cancel-in-progress: true -# Define the jobs that will be executed as part of the workflow jobs: - # Job to call and execute the 'test-code-playwright.yml' workflow call-workflow-playwright-test: - # Use a reusable GitHub Actions workflow from the 'button-inc/gh-actions' repository - # and the file 'test-code-playwright.yml' in the 'develop' branch - uses: button-inc/gh-actions/.github/workflows/test-code-playwright.yml@develop + uses: button-inc/gh-actions/.github/workflows/test-code-playwright.yml@v0.0.1 diff --git a/.github/workflows/scan-code.yml b/.github/workflows/scan-code.yml index 62ff715..eda9b3c 100644 --- a/.github/workflows/scan-code.yml +++ b/.github/workflows/scan-code.yml @@ -7,24 +7,24 @@ concurrency: cancel-in-progress: true jobs: - call-workflow-trivy-scan: - uses: button-inc/gh-actions/.github/workflows/scan-code-trivy.yml@develop + call-workflow-trivy-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-trivy.yml@v0.0.1 - call-workflow-husky-scan: - uses: button-inc/gh-actions/.github/workflows/scan-code-husky.yml@develop - with: - working-directory: ./app - node-version: '18' + call-workflow-husky-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-husky.yml@v0.0.1 + with: + working-directory: ./app + node-version: "18" - call-workflow-gitleaks-scan: - uses: button-inc/gh-actions/.github/workflows/scan-code-gitleaks.yml@develop - with: - notify-user-list: "@shon-button,@YaokunLin" - secrets: - github-token: ${{ secrets.GITHUB_TOKEN }} - gitleaks-license: ${{ secrets.GITLEAKS_LICENSE}} + call-workflow-gitleaks-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-gitleaks.yml@v0.0.1 + with: + notify-user-list: "@shon-button,@YaokunLin" + secrets: + github-token: ${{ secrets.GITHUB_TOKEN }} + gitleaks-license: ${{ secrets.GITLEAKS_LICENSE}} - call-workflow-owasp-zap-scan: - uses: button-inc/gh-actions/.github/workflows/scan-code-owasp-zap.yml@develop - with: - target-url: 'http://localhost:3000' + call-workflow-owasp-zap-scan: + uses: button-inc/gh-actions/.github/workflows/scan-code-owasp-zap.yml@v0.0.1 + with: + target-url: "http://localhost:3000" diff --git a/README.md b/README.md index d4ed0db..f81876b 100644 --- a/README.md +++ b/README.md @@ -218,8 +218,6 @@ Linux/Mac: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/keyfile.json export GOOGLE_APPLICATION_CREDENTIALS=credentials/service-account-key-gcs.json ``` -**Note**: see package.json\scripts\dev for an example of setting the GAC before starting the dev server. You will need to set .env.development\GAC_EXPORT value to the value of the [service account](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106707473171516793046?project=emissions-elt-demo) key location. Example: GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/credentials/service-account-key-gcs.json" - To echo the value of the **`GOOGLE_APPLICATION_CREDENTIALS`** environment variable in a terminal you can use the following command: Linux/Mac: @@ -317,6 +315,8 @@ Testing GCS file upload: pnpm run test:gcs ``` +**Note**: `pnpm run test:gcs` will run a script `scripts/tests/test-gcs.sh` that configures GOOGLE_APPLICATION_CREDENTIALS from service-account-key.json information stored as a string in `scripts\tests\.env` + ## Running App Locally ### run dev server diff --git a/app/logs/errors/file.json b/app/logs/errors/file.json index 8bd8647..31c45ea 100644 --- a/app/logs/errors/file.json +++ b/app/logs/errors/file.json @@ -146,3 +146,11 @@ {"timestamp":"2023-07-21T14:37:35.447Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} {"timestamp":"2023-07-21T14:48:15.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} {"timestamp":"2023-07-21T14:49:56.922Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-07-24T14:36:02.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-24T14:36:32.895Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-24T14:37:58.904Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-24T14:38:29.609Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected token 'e', \"ewogICJ0eX\"... is not valid JSON"} +{"timestamp":"2023-07-24T14:50:12.789Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"} +{"timestamp":"2023-07-24T14:50:43.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"} +{"timestamp":"2023-07-24T14:51:14.148Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"} +{"timestamp":"2023-07-24T15:01:08.578Z","type":"upload","error":"unsupported"} diff --git a/playwright/.auth/user.json b/playwright/.auth/user.json index 6595aac..1c92f35 100644 --- a/playwright/.auth/user.json +++ b/playwright/.auth/user.json @@ -15,7 +15,7 @@ "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ", "domain": "localhost", "path": "/", - "expires": 1690037382, + "expires": 1690297251, "httpOnly": true, "secure": false, "sameSite": "Lax" From b83b73134e73c91ec65fe512168588c13daf400a Mon Sep 17 00:00:00 2001 From: shon-button Date: Mon, 24 Jul 2023 14:48:27 -0400 Subject: [PATCH 05/14] updated gitignore --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index db6d24f..a2bd3ef 100644 --- a/.dockerignore +++ b/.dockerignore @@ -33,6 +33,8 @@ yarn-error.log* # local env files +.env +.env. .env._ .env_ From c361fb9144eeef754ba13f0b5c817cb9db5723d4 Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Mon, 24 Jul 2023 14:51:48 -0400 Subject: [PATCH 06/14] Delete .env --- scripts/tests/.env | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 scripts/tests/.env diff --git a/scripts/tests/.env b/scripts/tests/.env deleted file mode 100644 index 559722b..0000000 --- a/scripts/tests/.env +++ /dev/null @@ -1,2 +0,0 @@ -SERVICE_ACCOUNT_JSON='{"type":"service_account","project_id":"emissions-elt-demo","private_key_id":"ecc9c7e27bf42d989143745aea253a5f93e49aac","private_key":"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCT0/94jFq0fiwV\n9/FTyzc7SZ498sy3ZNJrXa0DRlnAP9YccbTBTCaa44So/xTrlEm5lP9XPd4CmKmD\nunRXGFo0L0wmyniO6HIeldclDMgzwOu4ptlreyL70Fc81hd2hre7Zu02xys2I4sw\nImieJ/tuWsRscYUgUeOLkIbPkG/UM42/XhYf9Kjx8cucQcui30WRwRxwbK3Mr/wr\nue9xyDgIFrE7ZBEiHA7OSt/Ae70FKqlAtMlWgR2LdXGts0Q1iA3Qy2kwPJMMPP5Y\n7KZJAq5wqRnx7WFNHqrAzkxcdYpVKVx+Q1loUJ9qM2lLhIKUS3mk7riO2PTOv0Fj\nfbz1fsXHAgMBAAECggEAMZSGWAuW9ndk2N9mUMjVFuzrhnJzJ8VIb5slBnanbnva\nl4KpcbVVM1jAqx+WiCadjYFEHKIS3oMOQ7CbCYUQ5/S/ETmSMrgSYmC2HmaJlRYM\n2UsYm9xaUOPBBpX1m5q2b8OnJtqpCwjjy3qW5Qia4xnNTGPMlxjv/OS12lLituQr\nFaaKnzo1cpGDSmtkIlyANV7NEfbe4b08fetPQOOUDMnQ4Cal78m8pURt5Ineps9a\nZ8sTPqU58fEe4oD7q/zbVF5DVltwq3xg/gjQAWwgOIve9E6TkCjjJH0Fli6miOi8\nWtHxu8MUpuHa6VpQGCvEs6jJtlPPMdr50E8N+37jFQKBgQDDMA4SBTQodYqQWi7x\nKQqkcnnALI2ZZVKUJlUzz+G0dL8d9zuYhYvDGUCFP2zd+TmID7RV3C4tJ4Rqt30q\n/Eg0WYCp+S5rCrmEl4q547Q54U3+zUR9IFgBsz2i8u7t0JX0B4QTCW9mQvlJA4pB\n+a1Hm+4/Khw6mn4NwylyakZrBQKBgQDB4pi3oRS8Cb34yRF6fhvycllAGy22z0lw\n88e6Tub8gq7dgiiw/+/ehPrBuqXczvK6gzRNO72oFHmz/UPSdW1xS58kauQ3W2tr\nbNdrqYOFkuO7ucQd8JGtEpI9MFZ8GT+Z672P7EFYKDqExyscXw7y0VqmcQkuiP52\n1IaCIIa/WwKBgQCLKQbPGECwm+T3uCSBsfYxeqCNP/aQqCmxEIdskkjkRNxBvBQU\nURptNeLHXYn71IWNGU1Ebd/KN8Nz5nBqJkZAdJOEG/FZReMwwm6Yy9yh652VDbpH\nz7iPNcC7HaL1kOJogrdKb06qRRPAV7LKCP3e8TByfk50BdPbcgpp1ZVxFQKBgEga\nMJj5enCDXvaKL8nR5CrBg5dnhBSb+b/bqMcMWLJHFyihIujQBTBHW8l30/7Np07d\nRDIEqX88PhZFdVdq/AxKByDP75b2lHgavfH31EV0XuSNLPXFZSdr5J6Ev2TfLtva\n42AGiDZ0n26JcurWHwUF/iQvnS6FG7ytRGhYGERJAoGAHxYwRdfjRCmdnbNvm1j3\n8KSkDWN6W2tEzgSJAQMf36G/ttbIdM93T/XY5xqfeC8cGZY732uFSkYkrRPboCA9\n9pMwctxacOr4uCsiFEV6PhFWu+hp9hMyHlgakrW4NVXb6GoU+DjruBX3ktLXBNe1\nJ+VrC317w6x8UdzvHHrEJ5g=\n-----END PRIVATE KEY-----\n","client_email":"cloud-storage-sa@emissions-elt-demo.iam.gserviceaccount.com","client_id":"106707473171516793046","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/cloud-storage-sa%40emissions-elt-demo.iam.gserviceaccount.com","universe_domain":"googleapis.com"}' -SERVICE_ACCOUNT_JSON_BASE64=ewogICJ0eXBlIjogInNlcnZpY2VfYWNjb3VudCIsCiAgInByb2plY3RfaWQiOiAiZW1pc3Npb25zLWVsdC1kZW1vIiwKICAicHJpdmF0ZV9rZXlfaWQiOiAiZWNjOWM3ZTI3YmY0MmQ5ODkxNDM3NDVhZWEyNTNhNWY5M2U0OWFhYyIsCiAgInByaXZhdGVfa2V5IjogIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxuTUlJRXZRSUJBREFOQmdrcWhraUc5dzBCQVFFRkFBU0NCS2N3Z2dTakFnRUFBb0lCQVFDVDAvOTRqRnEwZml3VlxuOS9GVHl6YzdTWjQ5OHN5M1pOSnJYYTBEUmxuQVA5WWNjYlRCVENhYTQ0U28veFRybEVtNWxQOVhQZDRDbUttRFxudW5SWEdGbzBMMHdteW5pTzZISWVsZGNsRE1nendPdTRwdGxyZXlMNzBGYzgxaGQyaHJlN1p1MDJ4eXMySTRzd1xuSW1pZUovdHVXc1JzY1lVZ1VlT0xrSWJQa0cvVU00Mi9YaFlmOUtqeDhjdWNRY3VpMzBXUndSeHdiSzNNci93clxudWU5eHlEZ0lGckU3WkJFaUhBN09TdC9BZTcwRktxbEF0TWxXZ1IyTGRYR3RzMFExaUEzUXkya3dQSk1NUFA1WVxuN0taSkFxNXdxUm54N1dGTkhxckF6a3hjZFlwVktWeCtRMWxvVUo5cU0ybExoSUtVUzNtazdyaU8yUFRPdjBGalxuZmJ6MWZzWEhBZ01CQUFFQ2dnRUFNWlNHV0F1VzluZGsyTjltVU1qVkZ1enJobkp6SjhWSWI1c2xCbmFuYm52YVxubDRLcGNiVlZNMWpBcXgrV2lDYWRqWUZFSEtJUzNvTU9RN0NiQ1lVUTUvUy9FVG1TTXJnU1ltQzJIbWFKbFJZTVxuMlVzWW05eGFVT1BCQnBYMW01cTJiOE9uSnRxcEN3amp5M3FXNVFpYTR4bk5UR1BNbHhqdi9PUzEybExpdHVRclxuRmFhS256bzFjcEdEU210a0lseUFOVjdORWZiZTRiMDhmZXRQUU9PVURNblE0Q2FsNzhtOHBVUnQ1SW5lcHM5YVxuWjhzVFBxVTU4ZkVlNG9EN3EvemJWRjVEVmx0d3EzeGcvZ2pRQVd3Z09JdmU5RTZUa0NqakpIMEZsaTZtaU9pOFxuV3RIeHU4TVVwdUhhNlZwUUdDdkVzNmpKdGxQUE1kcjUwRThOKzM3akZRS0JnUURETUE0U0JUUW9kWXFRV2k3eFxuS1Fxa2NubkFMSTJaWlZLVUpsVXp6K0cwZEw4ZDl6dVloWXZER1VDRlAyemQrVG1JRDdSVjNDNHRKNFJxdDMwcVxuL0VnMFdZQ3ArUzVyQ3JtRWw0cTU0N1E1NFUzK3pVUjlJRmdCc3oyaTh1N3QwSlgwQjRRVENXOW1RdmxKQTRwQlxuK2ExSG0rNC9LaHc2bW40Tnd5bHlha1pyQlFLQmdRREI0cGkzb1JTOENiMzR5UkY2Zmh2eWNsbEFHeTIyejBsd1xuODhlNlR1YjhncTdkZ2lpdy8rL2VoUHJCdXFYY3p2SzZnelJOTzcyb0ZIbXovVVBTZFcxeFM1OGthdVEzVzJ0clxuYk5kcnFZT0ZrdU83dWNRZDhKR3RFcEk5TUZaOEdUK1o2NzJQN0VGWUtEcUV4eXNjWHc3eTBWcW1jUWt1aVA1MlxuMUlhQ0lJYS9Xd0tCZ1FDTEtRYlBHRUN3bStUM3VDU0JzZll4ZXFDTlAvYVFxQ214RUlkc2tramtSTnhCdkJRVVxuVVJwdE5lTEhYWW43MUlXTkdVMUViZC9LTjhOejVuQnFKa1pBZEpPRUcvRlpSZU13d202WXk5eWg2NTJWRGJwSFxuejdpUE5jQzdIYUwxa09Kb2dyZEtiMDZxUlJQQVY3TEtDUDNlOFRCeWZrNTBCZFBiY2dwcDFaVnhGUUtCZ0VnYVxuTUpqNWVuQ0RYdmFLTDhuUjVDckJnNWRuaEJTYitiL2JxTWNNV0xKSEZ5aWhJdWpRQlRCSFc4bDMwLzdOcDA3ZFxuUkRJRXFYODhQaFpGZFZkcS9BeEtCeURQNzViMmxIZ2F2ZkgzMUVWMFh1U05MUFhGWlNkcjVKNkV2MlRmTHR2YVxuNDJBR2lEWjBuMjZKY3VyV0h3VUYvaVF2blM2Rkc3eXRSR2hZR0VSSkFvR0FIeFl3UmRmalJDbWRuYk52bTFqM1xuOEtTa0RXTjZXMnRFemdTSkFRTWYzNkcvdHRiSWRNOTNUL1hZNXhxZmVDOGNHWlk3MzJ1RlNrWWtyUlBib0NBOVxuOXBNd2N0eGFjT3I0dUNzaUZFVjZQaEZXdStocDloTXlIbGdha3JXNE5WWGI2R29VK0RqcnVCWDNrdExYQk5lMVxuSitWckMzMTd3Nng4VWR6dkhIckVKNWc9XG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4iLAogICJjbGllbnRfZW1haWwiOiAiY2xvdWQtc3RvcmFnZS1zYUBlbWlzc2lvbnMtZWx0LWRlbW8uaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iLAogICJjbGllbnRfaWQiOiAiMTA2NzA3NDczMTcxNTE2NzkzMDQ2IiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS9jbG91ZC1zdG9yYWdlLXNhJTQwZW1pc3Npb25zLWVsdC1kZW1vLmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAidW5pdmVyc2VfZG9tYWluIjogImdvb2dsZWFwaXMuY29tIgp9 \ No newline at end of file From 4f11460977b08d6445aa68485bcd5eda7ef92137 Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Mon, 24 Jul 2023 14:52:11 -0400 Subject: [PATCH 07/14] Delete .env --- tests/.env | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 tests/.env diff --git a/tests/.env b/tests/.env deleted file mode 100644 index 007171f..0000000 --- a/tests/.env +++ /dev/null @@ -1,4 +0,0 @@ -API_HOST=http://localhost:3000/ -GOOGLE_BUCKET_NAME=eed_upload_file_storage -NEXTAUTH_JWT_ANALYST= "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ" -NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk From 24eca9a8514994ce5b046be777209265048ad22b Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Mon, 24 Jul 2023 14:52:30 -0400 Subject: [PATCH 08/14] Delete .env --- .env | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 6b42d18..0000000 --- a/.env +++ /dev/null @@ -1,30 +0,0 @@ - -API_HOST=http://localhost:3000/ -DATABASE=eed -DATABASE_HOST=34.125.92.26 -DATABASE_PORT=5432 -DATABASE_PROTOCOL=postgres -DATABASE_SCHEMA_ADMIN=eed -DATABASE_SCHEMA_CLEAN=data_clean_room -DATABASE_SCHEMA_WORKSPACE=data_science_workspace -DATABASE_SCHEMA_VAULT=published_vault -DATABASE_USER_ADMIN=eed_internal -DATABASE_USER_PW_ADMIN=secret -DATABASE_USER_ANALYST=eed_internal -DATABASE_USER_PW_ANALYST=secret -DATABASE_USER_DROPPER=eed_internal -DATABASE_USER_PW_DROPPER=secret -DATABASE_USER_MANAGER=eed_internal -DATABASE_USER_PW_MANAGER=secret -DATABASE_INSTANCE_CONNECTION_NAME=emissions-elt-demo:us-west4:eed -DB_USERNAME=keycloak_actor -DB_PASSWORD=keycloak_pass -GAC_EXPORT="export GOOGLE_APPLICATION_CREDENTIALS=/home/shon/Workspace/Button/credentials/service-account-key-gcs.json" -GITHUB_ID=291382f53280c6de8f4d -GITHUB_SECRET=aaa22549c1066fbf1138505fa03a28e03ab045b8 -GOOGLE_BUCKET_NAME=eed_upload_file_storage -GOOGLE_CLIENT_ID=77682378143-061racvi899q8vvgmcioqhbnu8pokm9o.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=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 -GOOGLE_PROJECT_NAME="emissions-elt-demo" -NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_SECRET=ozloLCYWDJNdMl5nh91QNdSj3qMWwpRk \ No newline at end of file From 993bcd234303dd4bb203274d8dc4c31d11fbc40c Mon Sep 17 00:00:00 2001 From: shon-button Date: Mon, 31 Jul 2023 14:15:27 -0400 Subject: [PATCH 09/14] Updated .gitignore file to exclude specific files. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e25bb66..b8c7011 100644 --- a/.gitignore +++ b/.gitignore @@ -26,8 +26,9 @@ yarn-error.log* .pnpm-debug.log* # local env files +.env .env*.local -.env.* +.env*.development # vercel .vercel From decf72eb7efd724377dce66e352c0d0b20a2cba7 Mon Sep 17 00:00:00 2001 From: shon-button Date: Mon, 31 Jul 2023 14:18:21 -0400 Subject: [PATCH 10/14] Updated README with scripts/tests/test-gcs.sh to set the export GOOGLE_APPLICATION_CREDENTIALS --- .env.example | 22 ++++++++++++++++++++++ README.md | 3 ++- scripts/tests/.env.example | 1 + tests/.env.example | 4 ++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 scripts/tests/.env.example create mode 100644 tests/.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e2b7567 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ + +API_HOST= +DATABASE= +DATABASE_HOST= +DATABASE_PORT= +DATABASE_PROTOCOL= +DATABASE_SCHEMA_ADMIN= +DATABASE_SCHEMA_CLEAN= +DATABASE_SCHEMA_WORKSPACE= +DATABASE_SCHEMA_VAULT= +DATABASE_USER_ADMIN= +DATABASE_USER_PW_ADMIN= +DATABASE_USER_ANALYST= +DATABASE_USER_PW_ANALYST= +DATABASE_USER_DROPPER= +DATABASE_USER_PW_DROPPER= +DATABASE_USER_MANAGER= +DATABASE_USER_PW_MANAGER= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +NEXTAUTH_URL= +NEXTAUTH_SECRET= \ No newline at end of file diff --git a/README.md b/README.md index f81876b..5d90816 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,8 @@ Testing GCS file upload: pnpm run test:gcs ``` -**Note**: `pnpm run test:gcs` will run a script `scripts/tests/test-gcs.sh` that configures GOOGLE_APPLICATION_CREDENTIALS from service-account-key.json information stored as a string in `scripts\tests\.env` +**Note**: `pnpm run test:gcs` will run a script `scripts/tests/test-gcs.sh` that configures GOOGLE_APPLICATION_CREDENTIALS from service-account-key.json information stored as a stringify JSON object in `scripts\tests\.env`, for use of the [service account](https://console.cloud.google.com/iam-admin/serviceaccounts/details/106707473171516793046?project=emissions-elt-demo) permisions for GCS authentication. + ## Running App Locally diff --git a/scripts/tests/.env.example b/scripts/tests/.env.example new file mode 100644 index 0000000..4fcdf2a --- /dev/null +++ b/scripts/tests/.env.example @@ -0,0 +1 @@ +SERVICE_ACCOUNT_JSON= \ No newline at end of file diff --git a/tests/.env.example b/tests/.env.example new file mode 100644 index 0000000..3139868 --- /dev/null +++ b/tests/.env.example @@ -0,0 +1,4 @@ +API_HOST= +GOOGLE_BUCKET_NAME= +NEXTAUTH_JWT_ANALYST= +NEXTAUTH_SECRET= From f95ada17b838c46d71379f0f302fd0356d4a2503 Mon Sep 17 00:00:00 2001 From: YaokunLin Date: Mon, 31 Jul 2023 23:57:25 -0700 Subject: [PATCH 11/14] fixed import path --- tests/i18n/lng-from-cookie.spec.ts | 2 +- tests/i18n/lng-from-url-prefix.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/i18n/lng-from-cookie.spec.ts b/tests/i18n/lng-from-cookie.spec.ts index 34176d9..270c378 100644 --- a/tests/i18n/lng-from-cookie.spec.ts +++ b/tests/i18n/lng-from-cookie.spec.ts @@ -1,5 +1,5 @@ import { test, expect, chromium, BrowserContext, Page } from "@playwright/test"; -import { fallbackLng } from "../../../app/i18n/settings"; +import { fallbackLng } from "../../app/i18n/settings"; import { lngs, siteUrl } from "./testUtils"; const cookieName = "i18next"; diff --git a/tests/i18n/lng-from-url-prefix.spec.ts b/tests/i18n/lng-from-url-prefix.spec.ts index 65c41eb..a1ef0e5 100644 --- a/tests/i18n/lng-from-url-prefix.spec.ts +++ b/tests/i18n/lng-from-url-prefix.spec.ts @@ -6,7 +6,7 @@ import { BrowserContext, Page, } from "@playwright/test"; -import { fallbackLng } from "../../../app/i18n/settings"; +import { fallbackLng } from "../../app/i18n/settings"; import { EN_WELCOME_MSG, FR_WELCOME_MSG, From 6c93684221c745abaa362497d438cfe4c24321d7 Mon Sep 17 00:00:00 2001 From: shon-button Date: Fri, 4 Aug 2023 10:23:03 -0400 Subject: [PATCH 12/14] Update .gitignore to ignore all .env except env.example --- .gitignore | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b8c7011..a4a3d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,10 +25,10 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# local env files -.env -.env*.local -.env*.development +# all env files except .env.example +**/.env +!**/env.example + # vercel .vercel From 2543b5dc82ffd5753b1e6aeff64c8b227c0e4db0 Mon Sep 17 00:00:00 2001 From: shon-button Date: Fri, 4 Aug 2023 11:03:41 -0400 Subject: [PATCH 13/14] =?UTF-8?q?=F0=9F=93=9A=20Documented=20resolution=20?= =?UTF-8?q?for=20scripts/tests/test-gcs.sh=20error=20[next-auth][error][NO?= =?UTF-8?q?=5FSECRET]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 30 +++++++++------------- .env.example | 1 + .gitignore | 6 +++-- app/logs/errors/file.json | 52 +++++++++++++++++++++++++++++++++++++++ scripts/tests/test-gcs.sh | 1 + 5 files changed, 69 insertions(+), 21 deletions(-) diff --git a/.dockerignore b/.dockerignore index a2bd3ef..50ffe0e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,17 @@ +/node_modules +.env +.env.local +Dockerfile +.git +.gitignore +docker-compose\* + +# files form git-ignore + # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies -/node_modules /.pnp .pnp.js @@ -18,6 +27,7 @@ # production /build +/dist # misc @@ -31,13 +41,6 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# local env files - -.env -.env. -.env._ -.env_ - # vercel .vercel @@ -46,14 +49,3 @@ yarn-error.log* \*.tsbuildinfo next-env.d.ts -/test-results/ -/playwright-report/ -/playwright/.cache/ - -# keycloak - -keycloak-sa-credential.json - -# gcp - -\nplaywright/.auth diff --git a/.env.example b/.env.example index e2b7567..157d7e3 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,7 @@ DATABASE_USER_DROPPER= DATABASE_USER_PW_DROPPER= DATABASE_USER_MANAGER= DATABASE_USER_PW_MANAGER= +GOOGLE_BUCKET_NAME= GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= NEXTAUTH_URL= diff --git a/.gitignore b/.gitignore index a4a3d6f..a45f8f6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,8 @@ yarn-error.log* # all env files except .env.example **/.env -!**/env.example +**/.env.development +!**/.env.example # vercel @@ -46,4 +47,5 @@ keycloak-sa-credential.json emissions-elt-demo-ecc9c7e27bf4.json # playwright -/tests/.env \ No newline at end of file +/tests/.env +playwright/.auth/user.json diff --git a/app/logs/errors/file.json b/app/logs/errors/file.json index 31c45ea..6496242 100644 --- a/app/logs/errors/file.json +++ b/app/logs/errors/file.json @@ -154,3 +154,55 @@ {"timestamp":"2023-07-24T14:50:43.418Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"} {"timestamp":"2023-07-24T14:51:14.148Z","type":"upload","error":"Failed to upload file. SyntaxError: Unexpected end of JSON input"} {"timestamp":"2023-07-24T15:01:08.578Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-08-04T14:14:19.273Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:14:49.814Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:15:20.561Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:15:51.410Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:16:22.019Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:16:52.824Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:17:23.485Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:17:54.293Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:18:24.798Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:18:55.596Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:19:26.319Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:19:56.977Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:20:27.762Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:20:58.271Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:21:29.143Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:21:59.866Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:22:30.632Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:23:01.400Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:23:32.016Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:24:02.746Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:24:33.287Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:25:04.076Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:25:34.613Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:26:05.259Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:26:36.133Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:27:06.905Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:27:37.443Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:28:08.119Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:28:39.001Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:29:09.734Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:29:40.527Z","type":"upload","error":"unsupported"} +{"timestamp":"2023-08-04T14:31:56.370Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:32:27.051Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:32:57.966Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:33:28.444Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:33:59.138Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:34:30.087Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:35:00.549Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:35:31.296Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:36:02.165Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:36:32.916Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:37:03.643Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:37:34.348Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:38:04.849Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:38:35.465Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:39:06.384Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:39:37.146Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:40:07.844Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:40:38.347Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:41:09.160Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:42:13.000Z","type":"upload","error":"Failed to upload file. Error: A bucket name is needed to use Cloud Storage."} +{"timestamp":"2023-08-04T14:46:23.694Z","type":"upload","error":"unsupported"} diff --git a/scripts/tests/test-gcs.sh b/scripts/tests/test-gcs.sh index 568bec7..528f9a0 100755 --- a/scripts/tests/test-gcs.sh +++ b/scripts/tests/test-gcs.sh @@ -19,6 +19,7 @@ export GOOGLE_APPLICATION_CREDENTIALS="$TEMP_FILE" npm run build # Start the Next.js application in production mode +# For production mode, ensure a root .env file exists with the NEXTAUTH_SECRET to prevent error: [next-auth][error][NO_SECRET] https://next-auth.js.org/errors#no_secret Please define a `secret` in production. MissingSecret [MissingSecretError]: Please define a `secret` in production. npm run start & # Wait for a few seconds to ensure the Next.js application is ready From 7685f2d6e0dd271f35e5ed7ea58f8f203f234bd0 Mon Sep 17 00:00:00 2001 From: Shon <120038448+shon-button@users.noreply.github.com> Date: Fri, 4 Aug 2023 11:05:12 -0400 Subject: [PATCH 14/14] Delete user.json --- playwright/.auth/user.json | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 playwright/.auth/user.json diff --git a/playwright/.auth/user.json b/playwright/.auth/user.json deleted file mode 100644 index 1c92f35..0000000 --- a/playwright/.auth/user.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "cookies": [ - { - "name": "i18next", - "value": "en-US", - "domain": "localhost", - "path": "/", - "expires": -1, - "httpOnly": false, - "secure": false, - "sameSite": "Strict" - }, - { - "name": "next-auth.session-token", - "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..Sk5Sw0xZUG8HLBCI.RX24eO8DeJzbtIbkfosmbHund4BMOAZ0n-DVtXNTnvBGniSTpH-CSmJyf0wCY1xiyOCUr1-bHr-Zz-gx1cDCZIRyY8eGYpE-6fT09ly_tuaZ5TC9cxOKYDgZ-jHRVQigAD083I7p1f77DLUOWkJKYqH_6Thpwodw-MAeBFVEQNx0sod64zrxfUeuLyz-yPpz09zt-2oe9yh3GDoDEest-YgODArBDIrPliOytDNtElLnYlulZcH6vvWoqQwzG7mtTTdMYUNNvxWVnhkCw8IhvOf0J6m_K-Vc3XCr--KmqGy8EVLvX3QMeGyDorlcNvlOqNoqwZwvn02lKUWpYZzPp-AFe0La4sbSqrNl5yD0NvM-daA.68Oy5_QQGCS4zHl-RImlGQ", - "domain": "localhost", - "path": "/", - "expires": 1690297251, - "httpOnly": true, - "secure": false, - "sameSite": "Lax" - } - ], - "origins": [] -} \ No newline at end of file