diff --git a/apps/consent/cypress.config.ts b/apps/consent/cypress.config.ts index 9143d8168f..7e1bc5a143 100644 --- a/apps/consent/cypress.config.ts +++ b/apps/consent/cypress.config.ts @@ -18,4 +18,8 @@ export default defineConfig({ }, screenshotOnRunFailure: false, video: false, + retries: { + openMode: 1, + runMode: 2, + }, }) diff --git a/apps/consent/cypress/e2e/email-sign-in/login-email.cy.ts b/apps/consent/cypress/e2e/email-sign-in/login-email.cy.ts index 3ea4ec250f..751e14d36e 100644 --- a/apps/consent/cypress/e2e/email-sign-in/login-email.cy.ts +++ b/apps/consent/cypress/e2e/email-sign-in/login-email.cy.ts @@ -4,6 +4,7 @@ describe("Account ID Test", () => { let login_challenge: string | null before(() => { + cy.flushRedis() cy.visit(testData.AUTHORIZATION_URL) cy.url().then((currentUrl) => { const urlObj = new URL(currentUrl) diff --git a/apps/consent/cypress/e2e/phone-sign-in/login-phone.cy.ts b/apps/consent/cypress/e2e/phone-sign-in/login-phone.cy.ts index 17d9136f2d..11d7a9b6a9 100644 --- a/apps/consent/cypress/e2e/phone-sign-in/login-phone.cy.ts +++ b/apps/consent/cypress/e2e/phone-sign-in/login-phone.cy.ts @@ -4,6 +4,7 @@ describe("Account ID Test", () => { let login_challenge: string | null before(() => { + cy.flushRedis() cy.visit(testData.AUTHORIZATION_URL) cy.url().then((currentUrl) => { const urlObj = new URL(currentUrl) diff --git a/apps/consent/cypress/support/commands.ts b/apps/consent/cypress/support/commands.ts index 6d77917e8e..820e4921ac 100644 --- a/apps/consent/cypress/support/commands.ts +++ b/apps/consent/cypress/support/commands.ts @@ -42,6 +42,7 @@ declare namespace Cypress { interface Chainable { getOTP(email: string): Chainable requestEmailCode(email: string): Chainable + flushRedis(): Chainable } } @@ -71,3 +72,14 @@ Cypress.Commands.add("requestEmailCode", (email) => { return response.body.result }) }) + +Cypress.Commands.add("flushRedis", () => { + const command = `docker exec galoy-dev-redis-1 redis-cli FLUSHALL` + cy.exec(command).then((result) => { + if (result.code === 0) { + cy.log("Redis FLUSHALL executed successfully") + } else { + throw new Error("Failed to execute FLUSHALL on Redis") + } + }) +}) diff --git a/apps/dashboard/BUCK b/apps/dashboard/BUCK index ae4ee902ae..ebd3a7612d 100644 --- a/apps/dashboard/BUCK +++ b/apps/dashboard/BUCK @@ -1,6 +1,7 @@ load( "@toolchains//workspace-pnpm:macros.bzl", "dev_pnpm_task_binary", + "dev_pnpm_task_test", "build_node_modules", "next_build", "next_build_bin", @@ -15,7 +16,17 @@ dev_pnpm_task_binary( dev_pnpm_task_binary( name = "lint-fix", - command = "dev", + command = "lint:fix", +) + +dev_pnpm_task_binary( + name = "cypress-open", + command = "cypress:open", +) + +dev_pnpm_task_test( + name = "test-integration", + command = "cypress:run", ) export_file( diff --git a/apps/dashboard/cypress.config.ts b/apps/dashboard/cypress.config.ts new file mode 100644 index 0000000000..a799cbba01 --- /dev/null +++ b/apps/dashboard/cypress.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from "cypress" +import dotenv from "dotenv" + +dotenv.config({ path: "../../dev/.envs/next-auth-session.env" }) + +export default defineConfig({ + e2e: { + baseUrl: "http://localhost:3001", + }, + defaultCommandTimeout: 60000, + env: { + NEXT_AUTH_SESSION_TOKEN: process.env.NEXT_AUTH_SESSION_TOKEN, + }, + component: { + devServer: { + framework: "next", + bundler: "webpack", + }, + }, + screenshotOnRunFailure: false, + video: false, + retries: { + openMode: 1, + runMode: 2, + }, +}) diff --git a/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts b/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts new file mode 100644 index 0000000000..78e1dd4bfb --- /dev/null +++ b/apps/dashboard/cypress/e2e/api-keys/api-keys.cy.ts @@ -0,0 +1,47 @@ +import { testData } from "../../support/test-data" + +describe("Callback Test", () => { + beforeEach(() => { + cy.viewport(1920, 1080) + cy.setCookie("next-auth.session-token", testData.NEXT_AUTH_SESSION_TOKEN, { + secure: true, + }) + cy.visit("/") + cy.get("[data-testid=sidebar-api-keys-link]").should("exist") + cy.get("[data-testid=sidebar-api-keys-link]").should("be.visible") + cy.get("[data-testid=sidebar-api-keys-link]").should("not.be.disabled") + cy.get("[data-testid=sidebar-api-keys-link]").click() + }) + + it("Api Key creation Test", () => { + cy.get('[data-testid="create-api-add-btn"]').should("exist") + cy.get('[data-testid="create-api-add-btn"]').should("be.visible") + cy.get('[data-testid="create-api-add-btn"]').should("not.be.disabled") + cy.get('[data-testid="create-api-add-btn"]').click() + + cy.get('[data-testid="create-api-name-input"]').should("exist") + cy.get('[data-testid="create-api-name-input"]').should("be.visible") + cy.get('[data-testid="create-api-name-input"]').should("not.be.disabled") + cy.get('[data-testid="create-api-name-input"]').type("New API Key") + + cy.get('[data-testid="create-api-expire-select"]').should("exist") + cy.get('[data-testid="create-api-expire-select"]').should("be.visible") + cy.get('[data-testid="create-api-expire-select"]').should("not.be.disabled") + cy.get('[data-testid="create-api-expire-select"]').click() + + cy.get('[data-testid="create-api-expire-30-days-select"]').should("exist") + cy.get('[data-testid="create-api-expire-30-days-select"]').should("be.visible") + cy.get('[data-testid="create-api-expire-30-days-select"]').should("not.be.disabled") + cy.get('[data-testid="create-api-expire-30-days-select"]').click() + + cy.get('[data-testid="create-api-create-btn"]').should("exist") + cy.get('[data-testid="create-api-create-btn"]').should("be.visible") + cy.get('[data-testid="create-api-create-btn"]').should("not.be.disabled") + cy.get('[data-testid="create-api-create-btn"]').click() + + cy.get('[data-testid="create-api-close-btn"]').should("exist") + cy.get('[data-testid="create-api-close-btn"]').should("be.visible") + cy.get('[data-testid="create-api-close-btn"]').should("not.be.disabled") + cy.get('[data-testid="create-api-close-btn"]').click() + }) +}) diff --git a/apps/dashboard/cypress/e2e/callback/callback.cy.ts b/apps/dashboard/cypress/e2e/callback/callback.cy.ts new file mode 100644 index 0000000000..9ce8c7ae31 --- /dev/null +++ b/apps/dashboard/cypress/e2e/callback/callback.cy.ts @@ -0,0 +1,73 @@ +import { testData } from "../../support/test-data" + +describe("Callback Test", () => { + let callbackId: string | undefined + + beforeEach(() => { + cy.viewport(1920, 1080) + cy.setCookie("next-auth.session-token", testData.NEXT_AUTH_SESSION_TOKEN, { + secure: true, + }) + cy.visit("/") + + cy.get("[data-testid=sidebar-callback-link]").should("exist") + cy.get("[data-testid=sidebar-callback-link]").should("be.visible") + cy.get("[data-testid=sidebar-callback-link]").should("not.be.disabled") + cy.get("[data-testid=sidebar-callback-link]").click() + }) + + it("Callback Addition Test", () => { + cy.get("[data-testid=add-callback-btn]").should("exist") + cy.get("[data-testid=add-callback-btn]").should("be.visible") + cy.get("[data-testid=add-callback-btn]").should("not.be.disabled") + cy.get("[data-testid=add-callback-btn]").click() + + cy.get("[data-testid=add-callback-input]").should("exist") + cy.get("[data-testid=add-callback-input]").should("be.visible") + cy.get("[data-testid=add-callback-input]").should("not.be.disabled") + cy.get("[data-testid=add-callback-input]").type(testData.CALLBACK_URL) + + cy.get("[data-testid=add-callback-create-btn]").should("exist") + cy.get("[data-testid=add-callback-create-btn]").should("be.visible") + cy.get("[data-testid=add-callback-create-btn]").should("not.be.disabled") + cy.get("[data-testid=add-callback-create-btn]").click() + + cy.contains("td", testData.CALLBACK_URL) + .should("exist") + .parent("tr") + .invoke("attr", "data-testid") + .then((id) => { + callbackId = id + cy.log("Callback ID:", id) + }) + }) + + it("Callback Deletion Test", () => { + expect(callbackId).to.exist + cy.get(`[data-testid="delete-callback-btn-${callbackId}"]`) + .filter(":visible") + .should("exist") + cy.get(`[data-testid="delete-callback-btn-${callbackId}"]`) + .filter(":visible") + .should("be.visible") + cy.get(`[data-testid="delete-callback-btn-${callbackId}"]`) + .filter(":visible") + .should("not.be.disabled") + cy.get(`[data-testid="delete-callback-btn-${callbackId}"]`).filter(":visible").click() + + cy.get(`[data-testid="delete-callback-confirm-btn-${callbackId}"]`) + .filter(":visible") + .should("exist") + cy.get(`[data-testid="delete-callback-confirm-btn-${callbackId}"]`) + .filter(":visible") + .should("be.visible") + cy.get(`[data-testid="delete-callback-confirm-btn-${callbackId}"]`) + .filter(":visible") + .should("not.be.disabled") + cy.get(`[data-testid="delete-callback-confirm-btn-${callbackId}"]`) + .filter(":visible") + .click() + + cy.get(`[data-testid="${callbackId}"]`).should("not.exist") + }) +}) diff --git a/apps/dashboard/cypress/e2e/consent-integration.cy.ts b/apps/dashboard/cypress/e2e/consent-integration.cy.ts new file mode 100644 index 0000000000..1f16fb1b18 --- /dev/null +++ b/apps/dashboard/cypress/e2e/consent-integration.cy.ts @@ -0,0 +1,68 @@ +describe("Consent integration Test", () => { + const signInData = { + EMAIL: "test@galoy.io", + } + + before(() => { + cy.flushRedis() + cy.visit("/api/auth/signin") + }) + + it("Consent integration", () => { + cy.contains("button", "Sign in with Blink").should("exist") + cy.contains("button", "Sign in with Blink").should("be.visible") + cy.contains("button", "Sign in with Blink").should("not.be.disabled") + cy.contains("button", "Sign in with Blink").click() + + const email = signInData.EMAIL + cy.get("[data-testid=email_id_input]").should("exist") + cy.get("[data-testid=email_id_input]").should("be.visible") + cy.get("[data-testid=email_id_input]").should("not.be.disabled") + cy.get("[data-testid=email_id_input]").type(email) + + cy.get("[data-testid=email_login_next_btn]").should("exist") + cy.get("[data-testid=email_login_next_btn]").should("be.visible") + cy.get("[data-testid=email_login_next_btn]").should("not.be.disabled") + cy.get("[data-testid=email_login_next_btn]").click() + + cy.getOTP(email).then((otp) => { + const code = otp + cy.get("[data-testid=verification_code_input]").should("exist") + cy.get("[data-testid=verification_code_input]").should("be.visible") + cy.get("[data-testid=verification_code_input]").should("not.be.disabled") + cy.get("[data-testid=verification_code_input]").type(code) + + cy.contains("label", "read").should("exist") + cy.contains("label", "read").should("be.visible") + cy.contains("label", "read").should("not.be.disabled") + cy.contains("label", "read").should("not.be.disabled") + cy.contains("label", "read").click() + + cy.contains("label", "write").should("exist") + cy.contains("label", "write").should("be.visible") + cy.contains("label", "write").should("not.be.disabled") + cy.contains("label", "write").click() + + cy.get("[data-testid=submit_consent_btn]").should("exist") + cy.get("[data-testid=submit_consent_btn]").should("be.visible") + cy.get("[data-testid=submit_consent_btn]").should("not.be.disabled") + cy.get("[data-testid=submit_consent_btn]").click() + + cy.url().should("eq", Cypress.config().baseUrl + "/") + cy.getCookie("next-auth.session-token").then((cookie) => { + if (cookie && cookie.value) { + cy.writeFile( + "../../dev/.envs/next-auth-session.env", + `NEXT_AUTH_SESSION_TOKEN=${cookie.value}\n`, + { + flag: "w", + }, + ) + cy.log("Session token saved to next-auth-session.test") + } else { + cy.log("Session token not found") + } + }) + }) + }) +}) diff --git a/apps/dashboard/cypress/e2e/e2e.cy.ts b/apps/dashboard/cypress/e2e/e2e.cy.ts new file mode 100644 index 0000000000..ae8dfd79a5 --- /dev/null +++ b/apps/dashboard/cypress/e2e/e2e.cy.ts @@ -0,0 +1,2 @@ +import "./api-keys/api-keys.cy" +import "./callback/callback.cy" diff --git a/apps/dashboard/cypress/support/commands.ts b/apps/dashboard/cypress/support/commands.ts new file mode 100644 index 0000000000..b5f5060e04 --- /dev/null +++ b/apps/dashboard/cypress/support/commands.ts @@ -0,0 +1,71 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } + +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + getOTP(email: string): Chainable + requestEmailCode(email: string): Chainable + flushRedis(): Chainable + } +} + +Cypress.Commands.add("getOTP", (email) => { + const query = `docker exec -i galoy-dev-kratos-pg-1 psql -U dbuser -d default -t -c "SELECT body FROM courier_messages WHERE recipient='${email}' ORDER BY created_at DESC LIMIT 1;"` + cy.exec(query).then((result) => { + const rawMessage = result.stdout + const otpMatch = rawMessage.match(/(\d{6})/) + if (otpMatch && otpMatch[1]) { + return otpMatch[1] + } else { + throw new Error("OTP not found in the message") + } + }) +}) + +Cypress.Commands.add("flushRedis", () => { + const command = `docker exec galoy-dev-redis-1 redis-cli FLUSHALL` + cy.exec(command).then((result) => { + if (result.code === 0) { + cy.log("Redis FLUSHALL executed successfully") + } else { + throw new Error("Failed to execute FLUSHALL on Redis") + } + }) +}) diff --git a/apps/dashboard/cypress/support/e2e.ts b/apps/dashboard/cypress/support/e2e.ts new file mode 100644 index 0000000000..fdd4334330 --- /dev/null +++ b/apps/dashboard/cypress/support/e2e.ts @@ -0,0 +1,21 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +// eslint-disable-next-line import/no-unassigned-import +import "./commands" + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/apps/dashboard/cypress/support/test-data.ts b/apps/dashboard/cypress/support/test-data.ts new file mode 100644 index 0000000000..ea00897c80 --- /dev/null +++ b/apps/dashboard/cypress/support/test-data.ts @@ -0,0 +1,5 @@ +export const testData = { + NEXT_AUTH_SESSION_TOKEN: Cypress.env("NEXT_AUTH_SESSION_TOKEN"), + EMAIL: "test@galoy.io", + CALLBACK_URL: "https://www.google.com/", +} diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index c945c0b968..8928f790dd 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -7,7 +7,9 @@ "build": "next build", "start": "next start -p 3001", "lint:fix": "eslint --fix --ext .ts,.tsx .", - "codegen": "graphql-codegen --config codegen.yml" + "codegen": "graphql-codegen --config codegen.yml", + "cypress:run": "cypress run --spec './cypress/e2e/consent-integration.cy.ts' && cypress run --spec './cypress/e2e/e2e.cy.ts' ", + "cypress:open": "cypress run --spec './cypress/e2e/consent-integration.cy.ts' && cypress open" }, "dependencies": { "@apollo/client": "^3.8.4", @@ -52,6 +54,7 @@ "@types/react": "18.2.33", "@types/react-dom": "18.2.14", "autoprefixer": "10.4.16", + "cypress": "^13.3.0", "eslint": "8.52.0", "eslint-config-next": "14.0.1", "eslint_d": "13.0.0", diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index c714696378..1acc22260a 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -8,7 +8,7 @@ "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "bundler", + "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", diff --git a/dev/Tiltfile b/dev/Tiltfile index 4792f99da1..d46c498e77 100644 --- a/dev/Tiltfile +++ b/dev/Tiltfile @@ -44,6 +44,8 @@ local_resource( allow_parallel = True, resource_deps = [ "hydra-dashboard", + "svix", + "svix-pg", ], links = [ link("http://localhost:3001", "dashboard"), @@ -83,6 +85,19 @@ local_resource( ], ) +dashboard_test_target = "//apps/dashboard:test-integration" +local_resource( + "dashboard-test", + labels = ["test"], + auto_init = is_ci and "dashboard" in cfg.get("test", []), + cmd = "buck2 test {}".format(dashboard_test_target), + resource_deps = [ + "consent", + "api-keys", + "dashboard", + ], +) + local_resource( name='init-test-user', labels = ['test'], @@ -166,7 +181,9 @@ core_serve_env = { "LND1_DNS": "localhost", "LND1_RPCPORT": "10009", "LND1_NAME": "lnd1", - "LND1_TYPE": "offchain,onchain" + "LND1_TYPE": "offchain,onchain", + "SVIX_SECRET": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTE2NzIwMTQsImV4cCI6MjAwNzAzMjAxNCwibmJmIjoxNjkxNjcyMDE0LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.b9s0aWSisNdUNki4edabBEToLNSwjC9-AiJQr4J3y4E", + "SVIX_ENDPOINT": "http://localhost:8071", } api_target = "//core/api:api" @@ -288,8 +305,9 @@ docker_groups = { "apollo-router", "mongodb", "redis", + "stablesats", + "svix-pg", "svix", - "stablesats" ], "bitcoin": [ "lnd1", diff --git a/dev/docker-compose.deps.yml b/dev/docker-compose.deps.yml index 803ca3e4e2..9dcebc8895 100644 --- a/dev/docker-compose.deps.yml +++ b/dev/docker-compose.deps.yml @@ -259,17 +259,16 @@ services: platform: linux/amd64 environment: WAIT_FOR: "true" - SVIX_DB_DSN: "postgresql://postgres:postgres@svix-pg/postgres" - SVIX_JWT_SECRET: "8KjzRXrKkd9YFcNyqLSIY8JwiaCeRc6WK4UkMnSW" + SVIX_DB_DSN: postgresql://postgres:postgres@svix-pg/postgres + SVIX_JWT_SECRET: 8KjzRXrKkd9YFcNyqLSIY8JwiaCeRc6WK4UkMnSW SVIX_WHITELIST_SUBNETS: "[0.0.0.0/0]" - SVIX_QUEUE_TYPE: "memory" + SVIX_QUEUE_TYPE: memory depends_on: - svix-pg ports: - - "8071:8071" + - 8071:8071 extra_hosts: - - "bats-tests:host-gateway" - + - dockerhost-alias:host-gateway svix-pg: image: postgres:14.1 environment: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61535a344c..962a493ff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,9 @@ importers: autoprefixer: specifier: 10.4.16 version: 10.4.16(postcss@8.4.31) + cypress: + specifier: ^13.3.0 + version: 13.3.1 eslint: specifier: 8.52.0 version: 8.52.0