Skip to content

Commit

Permalink
馃獰 馃И E2E Tests for auto-detect schema changes (#20682)
Browse files Browse the repository at this point in the history
* Add support to get workspaceId, create/delete source/destination from cypress

* Test delete source

* commands/request -> commands/api

* Add util to convert chains to promises, cleanup api, use async/await on test

* Add the ability to create and delete a connection

* Add E2E test for non-breaking schema changes
* Update db queries with generator functions
* Update users table to include better columns and cursor
* Add name to ResetWarningModalSwitch
* Add test ID to review button in SchemaChangesDetected modal

* Add test for breaking change auto-detect flow

* Fix linting issues

* Clean up afterEach for autoDetectSchema

* schemaChangesButton should be disabled after clicking

* Append random strings to source, dest, and connection created by auto-detect e2e test

* Add E2E tests to verify status icon in list pages

* Ensure that the manual sync button is enabled or disabled depending on the schema change type

* Add test to verify non-breaking prefrence update

* Fix testid prop case in StatusCell

* Fix small issues in auto-detect schema tests

* Remove it.only from autoDetectSchema.spec

* Remove promisification of api requests in e2e

* Fix typing in apiRequest

* Cleanup requestWorkspaceId

* Update auto-detect e2e test to check where backdrop should not exist for non-breaking changes

* Remove promise util for cypress

* Fix column name in connection e2e spec

* Fix fields in connection spec to match the new users table. Add email records to users table
  • Loading branch information
edmundito committed Feb 2, 2023
1 parent a5452a6 commit c2890bf
Show file tree
Hide file tree
Showing 17 changed files with 572 additions and 46 deletions.
67 changes: 67 additions & 0 deletions airbyte-webapp-e2e-tests/cypress/commands/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
ConnectionGetBody,
Connection,
ConnectionCreateRequestBody,
ConnectionsList,
Destination,
DestinationsList,
Source,
SourceDiscoverSchema,
SourcesList,
} from "./types";
import { getWorkspaceId, setWorkspaceId } from "./workspace";

const getApiUrl = (path: string): string => `http://localhost:8001/api/v1${path}`;

const apiRequest = <T = void>(
method: Cypress.HttpMethod,
path: string,
payload?: Cypress.RequestBody,
expectedStatus = 200
): Cypress.Chainable<T> =>
cy.request(method, getApiUrl(path), payload).then((response) => {
expect(response.status).to.eq(expectedStatus, "response status");
return response.body;
});

export const requestWorkspaceId = () =>
apiRequest<{ workspaces: Array<{ workspaceId: string }> }>("POST", "/workspaces/list").then(
({ workspaces: [{ workspaceId }] }) => {
setWorkspaceId(workspaceId);
}
);

export const requestConnectionsList = () =>
apiRequest<ConnectionsList>("POST", "/connections/list", { workspaceId: getWorkspaceId() });

export const requestCreateConnection = (body: ConnectionCreateRequestBody) =>
apiRequest<Connection>("POST", "/web_backend/connections/create", body);

export const requestUpdateConnection = (body: Record<string, unknown>) =>
apiRequest<Connection>("POST", "/web_backend/connections/update", body);

export const requestGetConnection = (body: ConnectionGetBody) =>
apiRequest<Connection>("POST", "/web_backend/connections/get", body);

export const requestDeleteConnection = (connectionId: string) =>
apiRequest("POST", "/connections/delete", { connectionId }, 204);

export const requestSourcesList = () =>
apiRequest<SourcesList>("POST", "/sources/list", { workspaceId: getWorkspaceId() });

export const requestSourceDiscoverSchema = (sourceId: string) =>
apiRequest<SourceDiscoverSchema>("POST", "/sources/discover_schema", { sourceId, disable_cache: true });

export const requestCreateSource = (body: Record<string, unknown>) =>
apiRequest<Source>("POST", "/sources/create", body);

export const requestDeleteSource = (sourceId: string) => apiRequest("POST", "/sources/delete", { sourceId }, 204);

export const requestDestinationsList = () =>
apiRequest<DestinationsList>("POST", "/destinations/list", { workspaceId: getWorkspaceId() });

export const requestCreateDestination = (body: Record<string, unknown>) =>
apiRequest<Destination>("POST", "/destinations/create", body);

export const requestDeleteDestination = (destinationId: string) =>
apiRequest("POST", "/destinations/delete", { destinationId }, 204);
2 changes: 2 additions & 0 deletions airbyte-webapp-e2e-tests/cypress/commands/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./api";
export * from "./payloads";
66 changes: 66 additions & 0 deletions airbyte-webapp-e2e-tests/cypress/commands/api/payloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ConnectionCreateRequestBody } from "./types";
import { getWorkspaceId } from "./workspace";

type RequiredConnectionCreateRequestProps = "name" | "sourceId" | "destinationId" | "syncCatalog" | "sourceCatalogId";
type CreationConnectRequestParams = Pick<ConnectionCreateRequestBody, RequiredConnectionCreateRequestProps> &
Partial<Omit<ConnectionCreateRequestBody, RequiredConnectionCreateRequestProps>>;

export const getConnectionCreateRequest = (params: CreationConnectRequestParams): ConnectionCreateRequestBody => ({
geography: "auto",
namespaceDefinition: "source",
namespaceFormat: "${SOURCE_NAMESPACE}",
nonBreakingChangesPreference: "ignore",
operations: [],
prefix: "",
scheduleType: "manual",
status: "active",
...params,
});

export const getPostgresCreateSourceBody = (name: string) => ({
name,
sourceDefinitionId: "decd338e-5647-4c0b-adf4-da0e75f5a750",
workspaceId: getWorkspaceId(),
connectionConfiguration: {
ssl_mode: { mode: "disable" },
tunnel_method: { tunnel_method: "NO_TUNNEL" },
replication_method: { method: "Standard" },
ssl: false,
port: 5433,
schemas: ["public"],
host: "localhost",
database: "airbyte_ci_source",
username: "postgres",
password: "secret_password",
},
});

export const getE2ETestingCreateDestinationBody = (name: string) => ({
name,
workspaceId: getWorkspaceId(),
destinationDefinitionId: "2eb65e87-983a-4fd7-b3e3-9d9dc6eb8537",
connectionConfiguration: {
type: "LOGGING",
logging_config: {
logging_type: "FirstN",
max_entry_count: 100,
},
},
});

export const getPostgresCreateDestinationBody = (name: string) => ({
name,
workspaceId: getWorkspaceId(),
destinationDefinitionId: "25c5221d-dce2-4163-ade9-739ef790f503",
connectionConfiguration: {
ssl_mode: { mode: "disable" },
tunnel_method: { tunnel_method: "NO_TUNNEL" },
ssl: false,
port: 5434,
schema: "public",
host: "localhost",
database: "airbyte_ci_destination",
username: "postgres",
password: "secret_password",
},
});
75 changes: 75 additions & 0 deletions airbyte-webapp-e2e-tests/cypress/commands/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
export interface Connection {
connectionId: string;
destination: Destination;
destinationId: string;
isSyncing: boolean;
name: string;
scheduleType: string;
schemaChange: string;
source: Source;
sourceId: string;
status: "active" | "inactive" | "deprecated";
nonBreakingChangesPreference: "ignore" | "disable";
syncCatalog: SyncCatalog;
}

export interface ConnectionCreateRequestBody {
destinationId: string;
geography: string;
name: string;
namespaceDefinition: string;
namespaceFormat: string;
nonBreakingChangesPreference: "ignore" | "disable";
operations: unknown[];
prefix: string;
scheduleType: string;
sourceCatalogId: string;
sourceId: string;
status: "active";
syncCatalog: SyncCatalog;
}

export interface ConnectionGetBody {
connectionId: string;
withRefreshedCatalog?: boolean;
}

export interface ConnectionsList {
connections: Connection[];
}

export interface Destination {
destinationDefinitionId: string;
destinationName: string;
destinationId: string;
connectionConfiguration: Record<string, unknown>;
}

export interface DestinationsList {
destinations: Destination[];
}

export interface Source {
sourceDefinitionId: string;
sourceName: string;
sourceId: string;
connectionConfiguration: Record<string, unknown>;
}

export interface SourceDiscoverSchema {
catalog: SyncCatalog;
catalogId: string;
}

export interface SourcesList {
sources: Source[];
}

export interface SyncCatalog {
streams: SyncCatalogStream[];
}

export interface SyncCatalogStream {
config: Record<string, unknown>;
stream: Record<string, unknown>;
}
9 changes: 9 additions & 0 deletions airbyte-webapp-e2e-tests/cypress/commands/api/workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let _workspaceId: string;

export const setWorkspaceId = (workspaceId: string) => {
_workspaceId = workspaceId;
};

export const getWorkspaceId = () => {
return _workspaceId;
};
89 changes: 62 additions & 27 deletions airbyte-webapp-e2e-tests/cypress/commands/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,68 @@
export const createTable = (tableName: string, columns: string[]): string =>
`CREATE TABLE ${tableName}(${columns.join(", ")});`;

export const dropTable = (tableName: string) => `DROP TABLE IF EXISTS ${tableName}`;

export const alterTable = (tableName: string, params: { add?: string[]; drop?: string[] }): string => {
const adds = params.add ? params.add.map((add) => `ADD COLUMN ${add}`) : [];
const drops = params.drop ? params.drop.map((columnName) => `DROP COLUMN ${columnName}`) : [];
const alterations = [...adds, ...drops];

return `ALTER TABLE ${tableName} ${alterations.join(", ")};`;
};

export const insertIntoTable = (tableName: string, valuesByColumn: Record<string, unknown>): string => {
const keys = Object.keys(valuesByColumn);
const values = keys
.map((key) => valuesByColumn[key])
.map((value) => (typeof value === "string" ? `'${value}'` : value));

return `INSERT INTO ${tableName}(${keys.join(", ")}) VALUES(${values.join(", ")});`;
};

export const insertMultipleIntoTable = (tableName: string, valuesByColumns: Array<Record<string, unknown>>): string =>
valuesByColumns.map((valuesByColumn) => insertIntoTable(tableName, valuesByColumn)).join("\n");

// Users table
export const createUsersTableQuery = `
CREATE TABLE users(id SERIAL PRIMARY KEY, col1 VARCHAR(200));`;
export const insertUsersTableQuery = `
INSERT INTO public.users(col1) VALUES('record1');
INSERT INTO public.users(col1) VALUES('record2');
INSERT INTO public.users(col1) VALUES('record3');`;
export const createUsersTableQuery = createTable("public.users", [
"id SERIAL",
"name VARCHAR(200) NULL",
"email VARCHAR(200) NULL",
"updated_at TIMESTAMP",
"CONSTRAINT users_pkey PRIMARY KEY (id)",
]);
export const insertUsersTableQuery = insertMultipleIntoTable("public.users", [
{ name: "Abigail", email: "abigail@example.com", updated_at: "2022-12-19 00:00:00" },
{ name: "Andrew", email: "andrew@example.com", updated_at: "2022-12-19 00:00:00" },
{ name: "Kat", email: "kat@example.com", updated_at: "2022-12-19 00:00:00" },
]);

export const dropUsersTableQuery = `
DROP TABLE IF EXISTS users;`;
export const dropUsersTableQuery = dropTable("public.users");

// Cities table
export const createCitiesTableQuery = `
CREATE TABLE cities(city_code VARCHAR(8), city VARCHAR(200));`;

export const insertCitiesTableQuery = `
INSERT INTO public.cities(city_code, city) VALUES('BCN', 'Barcelona');
INSERT INTO public.cities(city_code, city) VALUES('MAD', 'Madrid');
INSERT INTO public.cities(city_code, city) VALUES('VAL', 'Valencia')`;

export const alterCitiesTableQuery = `
ALTER TABLE public.cities
DROP COLUMN "city_code",
ADD COLUMN "state" text,
ADD COLUMN "country" text;`;
export const dropCitiesTableQuery = `
DROP TABLE IF EXISTS cities;`;
export const createCitiesTableQuery = createTable("public.cities", ["city_code VARCHAR(8)", "city VARCHAR(200)"]);

export const insertCitiesTableQuery = insertMultipleIntoTable("public.cities", [
{
city_code: "BCN",
city: "Barcelona",
},
{ city_code: "MAD", city: "Madrid" },
{ city_code: "VAL", city: "Valencia" },
]);

export const alterCitiesTableQuery = alterTable("public.cities", {
add: ["state TEXT", "country TEXT"],
drop: ["city_code"],
});
export const dropCitiesTableQuery = dropTable("public.cities");

// Cars table
export const createCarsTableQuery = `
CREATE TABLE cars(id SERIAL PRIMARY KEY, mark VARCHAR(200), model VARCHAR(200), color VARCHAR(200));`;
export const dropCarsTableQuery = `
DROP TABLE IF EXISTS cars;`;
export const createCarsTableQuery = createTable("public.cars", [
"id SERIAL PRIMARY KEY",
"mark VARCHAR(200)",
"model VARCHAR(200)",
"color VARCHAR(200)",
]);

export const dropCarsTableQuery = dropTable("public.cars");

0 comments on commit c2890bf

Please sign in to comment.