Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tests/api/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ AWS_REGION=eu-west-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

NAME_SLUG=
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Don't forget to add a value in the pipeline.

91 changes: 72 additions & 19 deletions tests/api/helpers/hasura.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import crypto from "crypto";
import { GraphQLClient } from "graphql-request";

// GraphQL client with admin privileges for creating test data
Expand Down Expand Up @@ -75,6 +76,14 @@ const DELETE_USER_MUTATION = `
}
`;

const FIND_USER_BY_AUTH0_QUERY = `
query FindUserByAuth0Id($auth0Id: String!) {
user(where: {auth0Id: {_eq: $auth0Id}}) {
id
}
}
`;

// Simple GraphQL mutation for creating app_metadata
const CREATE_APP_METADATA_MUTATION = `
mutation CreateAppMetadata($object: app_metadata_insert_input!) {
Expand Down Expand Up @@ -235,13 +244,13 @@ export const createTestTeam = async (name: string) => {
};

// Helper for creating test user
export const createTestUser = async (email: string, teamId: string) => {
export const createTestUser = async (email: string, teamId?: string) => {
try {
const response = (await adminGraphqlClient.request(CREATE_USER_MUTATION, {
object: {
email,
auth0Id: `auth0|test_${Date.now()}`,
team_id: teamId,
...(teamId && { team_id: teamId }),
ironclad_id: `ironclad_test_${Date.now()}`,
world_id_nullifier: `0x${Date.now().toString(16)}`,
},
Expand Down Expand Up @@ -285,6 +294,26 @@ export const deleteTestTeam = async (teamId: string) => {
}
};

// Helper for finding user by auth0Id
export const findUserByAuth0Id = async (auth0Id: string) => {
try {
const response = (await adminGraphqlClient.request(
FIND_USER_BY_AUTH0_QUERY,
{
auth0Id,
},
)) as any;

return response.user?.[0]?.id || null;
} catch (error: any) {
const errorMessage =
error?.response?.data?.message || error?.message || "Unknown error";
throw new Error(
`Failed to find user by auth0Id ${auth0Id}: ${errorMessage}`,
);
}
};

// Helper for deleting test user
export const deleteTestUser = async (userId: string) => {
try {
Expand Down Expand Up @@ -455,30 +484,54 @@ export const deleteTestLocalisation = async (localisationId: string) => {
}
};

// Helper for creating test API key
// Helper for creating test API key with real credentials for HTTP auth
export const createTestApiKey = async (
teamId: string,
name: string = "Test API Key",
) => {
try {
const response = (await adminGraphqlClient.request(
CREATE_API_KEY_MUTATION,
{
object: {
team_id: teamId,
name,
api_key: "test_hashed_secret_value",
is_active: true,
},
const AUTH0_SECRET = process.env.AUTH0_SECRET;
if (!AUTH0_SECRET) {
throw new Error("AUTH0_SECRET env var must be set for tests");
}

// Step 1: Generate friendly ID in Hasura format (key_xxxxxxxxxx)
const randomId = crypto.randomBytes(8).toString("hex");
const keyId = `key_${randomId}`;

// Step 2: Generate secret and hash using the pre-generated ID
const secret = `sk_${crypto.randomBytes(24).toString("hex")}`;
const hmac = crypto.createHmac("sha256", AUTH0_SECRET);
hmac.update(`${keyId}.${secret}`);
const hashed_secret = hmac.digest("hex");

// Step 3: Create API key record with the generated ID and hash in one operation
const createResponse = (await adminGraphqlClient.request(
CREATE_API_KEY_MUTATION,
{
object: {
id: keyId, // Use our pre-generated ID
team_id: teamId,
api_key: hashed_secret,
name,
is_active: true,
},
)) as any;
},
)) as any;

return response.insert_api_key_one?.id;
} catch (error: any) {
const errorMessage =
error?.response?.data?.message || error?.message || "Unknown error";
throw new Error(`Failed to create test API key: ${errorMessage}`);
if (!createResponse.insert_api_key_one?.id) {
throw new Error("Failed to create API key record");
}

// Step 4: Create proper API key header format for HTTP auth
const credentials = `${keyId}:${secret}`;
const encodedCredentials = Buffer.from(credentials).toString("base64");
const apiKeyHeader = `api_${encodedCredentials}`;

return {
apiKeyId: keyId,
secret,
apiKeyHeader,
};
};

// Helper for deleting test API key
Expand Down
2 changes: 1 addition & 1 deletion tests/api/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dotenv.config({ path: ".env.development" });

const config: Config = {
preset: "ts-jest",
setupFilesAfterEnv: ["jest-expect-message"],
setupFilesAfterEnv: ["jest-expect-message", "<rootDir>/jest.setup.ts"],
testMatch: ["<rootDir>/specs/**/*.spec.ts"],
testTimeout: 30000,
moduleDirectories: ["node_modules", "<rootDir>", "<rootDir>/../../web"],
Expand Down
16 changes: 16 additions & 0 deletions tests/api/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Global setup for API tests - validates required environment variables

const requiredEnvVars = ["INTERNAL_API_URL", "NAME_SLUG"];

beforeAll(() => {
const missingEnvVars = requiredEnvVars.filter(
(envVar) => !process.env[envVar],
);

if (missingEnvVars.length > 0) {
throw new Error(
`Required environment variables are not set: ${missingEnvVars.join(", ")}\n` +
"Please check your environment configuration.",
);
}
});
90 changes: 90 additions & 0 deletions tests/api/specs/dev-portal-helpers/create-action.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import axios from "axios";
import {
createTestApiKey,
createTestApp,
createTestTeam,
createTestUser,
deleteTestAction,
deleteTestApiKey,
deleteTestApp,
deleteTestTeam,
deleteTestUser,
} from "helpers";

const INTERNAL_API_URL = process.env.INTERNAL_API_URL;
const NAME_SLUG = process.env.NAME_SLUG;

describe("Dev Portal Helpers API Endpoints", () => {
describe("POST /api/v2/create-action/[app_id]", () => {
let cleanUpFunctions: Array<() => Promise<unknown>> = [];

afterEach(async () => {
await cleanUpFunctions.reduce<Promise<unknown>>(
(promise, callback) => promise.then(() => callback()),
Promise.resolve(),
);

cleanUpFunctions = [];
});

it("Create Action Successfully with API Key", async () => {
// Setup test data
const teamId = await createTestTeam("Test Team");
cleanUpFunctions.push(async () => await deleteTestTeam(teamId));

const userEmail = `qa+${NAME_SLUG}+${Date.now()}@toolsforhumanity.com`;
const userId = await createTestUser(userEmail, teamId);
cleanUpFunctions.push(async () => await deleteTestUser(userId));

const appId = await createTestApp("Test App", teamId);
cleanUpFunctions.push(async () => await deleteTestApp(appId));

// Create API key for authentication
const { apiKeyId, apiKeyHeader } = await createTestApiKey(
teamId,
"Test Key for Create Action",
);
cleanUpFunctions.push(async () => await deleteTestApiKey(apiKeyId));

// Test data
const actionData = {
action: `test_action_${Date.now()}`,
name: "Test Action",
description: "Test action description",
max_verifications: 5,
};

const response = await axios.post(
`${INTERNAL_API_URL}/api/v2/create-action/${appId}`,
actionData,
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKeyHeader}`,
},
},
);

expect(response.status).toBe(200);
expect(response.data).toEqual(
expect.objectContaining({
action: expect.objectContaining({
id: expect.any(String),
action: actionData.action,
name: actionData.name,
description: actionData.description,
max_verifications: actionData.max_verifications,
external_nullifier: expect.any(String),
status: "active",
}),
}),
);

// Extract action_id from response for cleanup
const createdActionId = response.data.action.id;
cleanUpFunctions.push(
async () => await deleteTestAction(createdActionId),
);
});
});
});
136 changes: 136 additions & 0 deletions tests/api/specs/dev-portal-helpers/create-team.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import axios from "axios";
import {
createTestUser,
deleteTestTeam,
deleteTestUser,
findUserByAuth0Id,
} from "helpers";
import { createAppSession } from "helpers/auth0";

const INTERNAL_API_URL = process.env.INTERNAL_API_URL;
const NAME_SLUG = process.env.NAME_SLUG;

describe("Dev Portal Helpers API Endpoints", () => {
describe("POST /api/create-team", () => {
let cleanUpFunctions: Array<() => Promise<unknown>> = [];

afterEach(async () => {
await cleanUpFunctions.reduce<Promise<unknown>>(
(promise, callback) => promise.then(() => callback()),
Promise.resolve(),
);

cleanUpFunctions = [];
});

it("Create team successfully for existing user", async () => {
const userEmail = `qa+${NAME_SLUG}+${Date.now()}@toolsforhumanity.com`;
const testUserId = await createTestUser(userEmail);

// Add cleanup functions
cleanUpFunctions.push(async () => await deleteTestUser(testUserId));

// Generate unique auth0Id to prevent constraint violations
const uniqueAuth0Id = `auth0|test_existing_user_${Date.now()}`;

const existingUserSession = await createAppSession({
user: {
sub: uniqueAuth0Id,
email: userEmail,
hasura: {
id: testUserId,
},
},
});

const teamData = {
team_name: "Test Team for API Tests",
hasUser: true,
};

const response = await axios.post(
`${INTERNAL_API_URL}/api/create-team`,
teamData,
{
headers: {
"Content-Type": "application/json",
Cookie: existingUserSession,
},
},
);

expect(
response.status,
`Create team request resolved with a wrong code:\n${JSON.stringify(response.data, null, 2)}`,
).toBe(200);

expect(response.data).toEqual(
expect.objectContaining({
returnTo: expect.stringMatching(/^\/teams\/team_[a-f0-9]{32}$/),
}),
);

// Extract team_id from returnTo URL for cleanup
const createdTeamId = response.data.returnTo.split("/teams/")[1];
cleanUpFunctions.push(async () => await deleteTestTeam(createdTeamId));
});

it("Create an initial team along with the user", async () => {
const userEmail = `qa+${NAME_SLUG}+${Date.now()}@toolsforhumanity.com`;
const teamData = {
team_name: "Test Team for New User",
hasUser: false,
};

// Generate unique auth0Id to prevent constraint violations
const uniqueAuth0Id = `auth0|test_new_user_${Date.now()}`;

const auth0Session = {
user: {
sub: uniqueAuth0Id,
email: userEmail,
hasura: {
id: `test_hasura_user_id_${Date.now()}`,
},
},
};

const sessionCookie = await createAppSession(auth0Session);

const response = await axios.post(
`${INTERNAL_API_URL}/api/create-team`,
teamData,
{
headers: {
"Content-Type": "application/json",
Cookie: sessionCookie,
},
},
);

expect(
response.status,
`Create team request resolved with a wrong code:\n${JSON.stringify(response.data, null, 2)}`,
).toBe(200);
expect(response.data).toEqual(
expect.objectContaining({
returnTo: expect.stringMatching(
/^\/teams\/team_[a-f0-9]{32}\/apps\/$/,
),
}),
);

// Extract team_id from returnTo URL for cleanup
const createdTeamId = response.data.returnTo
.split("/teams/")[1]
.split("/apps/")[0];
cleanUpFunctions.push(async () => await deleteTestTeam(createdTeamId));

// Find and cleanup the created user by auth0Id
const createdUserId = await findUserByAuth0Id(uniqueAuth0Id);
if (createdUserId) {
cleanUpFunctions.push(async () => await deleteTestUser(createdUserId));
}
});
});
});
Loading