Skip to content

Commit

Permalink
257 add cypress tests that use the integration db (#295)
Browse files Browse the repository at this point in the history
* init cypress integration

* assert-free-ports should exit 0 if nothing is running on :3000 or :3883, otherwise it should exit 1.  the commands after it in yarn should run with && not ;

* refactor big block of shell into "yarn integration:servers:start"

* can i also refactor "ci:test" to use "integration:servers:start" ?
the difference is adding MAILING_CLI=true, was that ommitted intentionally or no?

* add precheck for servers running to test:integration:cypress

* move ./scripts/assert-free-ports.sh check to integration:servers:start

* now you can't fuck it up

* hook for db:reset

* demo of a function that counts

* beforeAll is just called "before" in cypress

* small refactor for readability

* resolve typescript declared twice warning

* working clear dev database, imports are all screwy

* working clear web db

* wtf well i guess it allows some imports?

* add headed option

* add cy test that duplicates the bug

* fix bug on /settings page

* will it run in github

* oops fix ts

* add a login test

* test api key functionality

* test logout

* test login form errors

* test signup form errors

* fix github action

* try using cypress-io/github-action@v4

* oops

* duh yarn

* rm commented
  • Loading branch information
alexfarrill authored Oct 28, 2022
1 parent 038ccdd commit 1499e90
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 7 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/cypress_integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Cypress Integration Tests

on:
push:

jobs:
cypress:
runs-on: ubuntu-latest
services:
# Label used to access the service container
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: postgres
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
env:
MAILING_DATABASE_URL_TEST: postgresql://postgres:postgres@localhost:5432/mailing_cli_test
WEB_DATABASE_URL_TEST: postgresql://postgres:postgres@localhost:5432/mailing_test
MAILING_WEB_SESSION_PASSWORD: test-YEAed65EasPz7zbGF4fuoMTDka7C
MAILING_SESSION_PASSWORD: test-5toTch6whgDvErN1nPg0hbDq6Do6
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18
cache: "yarn"
- name: Install dependencies
run: yarn
- name: Build the app
run: yarn build
- name: Cypress run integration tests
run: yarn test:integration:cypress
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"test:integration:watch": "yarn integration:servers:start \"yarn link:emails; yarn jest -c jest.integration.config.ts --watch\"",
"ci:test": "yarn integration:servers:start test",
"ci:test:integration": "yarn integration:servers:start \"yarn link:emails; yarn jest -c jest.integration.config.ts\"",
"test:integration:cypress": "yarn integration:servers:start \"cd packages/cli && yarn cypress run -C cypressIntegration.config.ts\"",
"test:integration:cypress:headed": "yarn integration:servers:start \"cd packages/cli && yarn cypress run -C cypressIntegration.config.ts --headed\"",
"ci:cli": "cd packages/cli && yarn ci:server",
"ci:web": "cd packages/web && yarn ci:server",
"test:e2e": "bundle && bundle exec ruby e2e/cli.rb run-all",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
describe("login tests", () => {
before(() => {
cy.task("db:reset");
});

it("should be able to signup, login, and everything else", () => {
const email = "[email protected]";
cy.visit("/signup");
cy.location("pathname").should("eq", "/signup");

cy.get("h1").should("contain", "Signup");
cy.get("input#email").should("exist");

// invalid email should give an error
cy.get("input#email").type("test");
cy.get("form").submit();
cy.get(".form-error").should("contain", "email is invalid");

// invalid password (blank) should give an error
cy.get("input#email").clear().type(email);
cy.get("form").submit();
cy.get(".form-error").should(
"contain",
"password should be at least 8 characters"
);

// fill in email and passord fields with valid values and then submit the form
cy.get("input#email").clear().type(email);
cy.get("input#password").type("password");
cy.get("form").submit();

// it should redirect to the login page
cy.location("pathname").should("eq", "/login");
cy.get("h1").should("contain", "Login");

// fill in email and passord fields and then submit the form
cy.get("input#email").type(email);
cy.get("input#password").type("password");
cy.get("form").submit();

// it should redirect you to the settings page
cy.location("pathname").should("eq", "/settings");
cy.get("h1").should("contain", "Settings");

// you should see a default api key that was created
cy.get("#api-keys tbody tr").should("have.length", 1);

// you should see a button to add an API key
cy.get("button").should("contain", "New API Key");

// click the button to add an API key
cy.get("button").click();

// you should see 2 api keys in the tbody instead of 1
cy.get("#api-keys tbody tr").should("have.length", 2);

// you should get a 404 if you try to go back to /signup, only 1 user is allowed to signup
cy.visit("/signup", { failOnStatusCode: false });
cy.request({
url: "/signup",
followRedirect: false,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(404);
expect(response.redirectedToUrl).to.eq(undefined);
});

// logout
cy.request({
url: "/api/logout",
}).then((response) => {
expect(response.status).to.eq(200);
});

cy.visit("/settings", { failOnStatusCode: false });
cy.request({
url: "/settings",
followRedirect: false,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(307);
expect(response.redirectedToUrl).to.eq("http://localhost:3883/login");
});

// you can visit the login page
cy.visit("/login");
cy.location("pathname").should("eq", "/login");
cy.get("h1").should("contain", "Login");

// it should give you an error if you try to login with the wrong password
cy.get("input#email").type(email);
cy.get("input#password").type("wrongpassword");
cy.get("form").submit();

cy.get(".form-error").should("contain", "invalid password");
});

it("should show the user an error message if they try to login with an invalid email", () => {
// visit the login page
cy.visit("/login");
cy.location("pathname").should("eq", "/login");
cy.get("h1").should("contain", "Login");

// fill in email and passord fields and then submit the form
cy.get("input#email").type("[email protected]");
cy.get("input#password").type("password");
cy.get("form").submit();

// it should show an error message
cy.get("div.form-error").should(
"contain",
"no user exists with that email"
);
});

it("login is required on /settings", () => {
cy.visit("/settings", { failOnStatusCode: false });
cy.request({
url: "/settings",
followRedirect: false,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.eq(307);
expect(response.redirectedToUrl).to.eq("http://localhost:3883/login");
});
});
});
48 changes: 48 additions & 0 deletions packages/cli/cypressIntegration.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { defineConfig } from "cypress";
import cliPrisma from "./prisma";
import webPrisma from "../../packages/web/prisma";
import type { PrismaTableName } from "../../testUtilIntegration";

const resetDb = async () => {
await truncateDatabases();
return null;
};

export default defineConfig({
e2e: {
specPattern: "**/*.cy.integration.{js,jsx,ts,tsx}",
defaultCommandTimeout: 10000,
baseUrl: "http://localhost:3883",
setupNodeEvents(on, _config) {
on("task", {
"db:reset": resetDb,
});
},
},
});

// copied from testUtilIntegration.ts

export async function truncateCliDatabase() {
await truncateTables(cliPrisma);
}

export async function truncateWebDatabase() {
await truncateTables(webPrisma);
}

export async function truncateDatabases() {
return Promise.all([truncateCliDatabase(), truncateWebDatabase()]);
}

async function truncateTables(client: any) {
const tables =
(await client.$queryRaw`SELECT table_name FROM information_schema.tables where table_schema = 'public' AND table_name NOT like '_prisma%';`) as PrismaTableName[];

const joinedTableNames = tables
.map((t: PrismaTableName) => `"${t.table_name}"`)
.join(", ");

const query = `TRUNCATE ${joinedTableNames} CASCADE;`;
await client.$executeRawUnsafe(query);
}
2 changes: 1 addition & 1 deletion packages/cli/src/pages/components/FormError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function FormError(props: { children?: ReactNode }) {
if (!props.children) return null;

return (
<div className="bg-red-300 col-span-3 rounded-md py-2 px-3">
<div className="bg-red-300 col-span-3 rounded-md py-2 px-3 form-error">
{props.children}
</div>
);
Expand Down
7 changes: 4 additions & 3 deletions packages/cli/src/pages/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { withSessionSsr } from "../util/session";
import { useState } from "react";
import prisma from "../../prisma";
import { ApiKey, User } from "../../prisma/generated/client";
import type { ApiKey, User } from "../../prisma/generated/client";

export const getServerSideProps = withSessionSsr<{ user: any }>(
async function ({ req }) {
Expand All @@ -16,8 +16,9 @@ export const getServerSideProps = withSessionSsr<{ user: any }>(
};
}

const apiKeys: ApiKey[] = await prisma.apiKey.findMany({
const apiKeys = await prisma.apiKey.findMany({
where: { organizationId: user.organizationId },
select: { id: true, active: true },
});

return {
Expand Down Expand Up @@ -78,7 +79,7 @@ function Settings(props: { user: User; apiKeys: ApiKey[] }) {
</button>
</div>
<div className="col-span-3">
<table className="table-auto w-full">
<table id="api-keys" className="table-auto w-full">
<thead className="text-xs uppercase text-gray-500 border-t border-slate-300">
<tr>
<td>API Key</td>
Expand Down
3 changes: 1 addition & 2 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"jest-mock-extended": "^2.0.7",
"postcss": "^8.4.14",
"prisma": "^4.2.0",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4"
"tailwindcss": "^3.1.6"
}
}
2 changes: 1 addition & 1 deletion testUtilIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export async function disconnectDatabases() {
delete global.prismaMailingWeb;
}

interface PrismaTableName {
export interface PrismaTableName {
table_name: string;
}

Expand Down

2 comments on commit 1499e90

@vercel
Copy link

@vercel vercel bot commented on 1499e90 Oct 28, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

web – ./packages/web

web-sofn.vercel.app
web-rho-puce.vercel.app
web-git-main-sofn.vercel.app
www.mailing.run
mailing.run

@vercel
Copy link

@vercel vercel bot commented on 1499e90 Oct 28, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.