diff --git a/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts b/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts new file mode 100644 index 000000000..3234503e5 --- /dev/null +++ b/backend/e2e-test/routes/v1/secret-approval-policy.spec.ts @@ -0,0 +1,35 @@ +import { seedData1 } from "@app/db/seed-data"; + +const createPolicy = async (dto: { name: string; secretPath: string; approvers: string[]; approvals: number }) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/secret-approvals`, + headers: { + authorization: `Bearer ${jwtAuthToken}` + }, + body: { + workspaceId: seedData1.project.id, + environment: seedData1.environment.slug, + name: dto.name, + secretPath: dto.secretPath, + approvers: dto.approvers, + approvals: dto.approvals + } + }); + + expect(res.statusCode).toBe(200); + return res.json().approval; +}; + +describe("Secret approval policy router", async () => { + test("Create policy", async () => { + const policy = await createPolicy({ + secretPath: "/", + approvals: 1, + approvers: [seedData1.id], + name: "test-policy" + }); + + expect(policy.name).toBe("test-policy"); + }); +}); diff --git a/backend/e2e-test/routes/v1/secret-import.spec.ts b/backend/e2e-test/routes/v1/secret-import.spec.ts index c184e44e5..d84afef30 100644 --- a/backend/e2e-test/routes/v1/secret-import.spec.ts +++ b/backend/e2e-test/routes/v1/secret-import.spec.ts @@ -1,56 +1,23 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports"; +import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + import { seedData1 } from "@app/db/seed-data"; -const createSecretImport = async (importPath: string, importEnv: string) => { - const res = await testServer.inject({ - method: "POST", - url: `/api/v1/secret-imports`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - path: "/", - import: { - environment: importEnv, - path: importPath - } - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("secretImport"); - return payload.secretImport; -}; - -const deleteSecretImport = async (id: string) => { - const res = await testServer.inject({ - method: "DELETE", - url: `/api/v1/secret-imports/${id}`, - headers: { - authorization: `Bearer ${jwtAuthToken}` - }, - body: { - workspaceId: seedData1.project.id, - environment: seedData1.environment.slug, - path: "/" - } - }); - - expect(res.statusCode).toBe(200); - const payload = JSON.parse(res.payload); - expect(payload).toHaveProperty("secretImport"); - return payload.secretImport; -}; - describe("Secret Import Router", async () => { test.each([ { importEnv: "prod", importPath: "/" }, // one in root { importEnv: "staging", importPath: "/" } // then create a deep one creating intermediate ones ])("Create secret import $importEnv with path $importPath", async ({ importPath, importEnv }) => { // check for default environments - const payload = await createSecretImport(importPath, importEnv); + const payload = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath, + importEnv + }); expect(payload).toEqual( expect.objectContaining({ id: expect.any(String), @@ -62,12 +29,32 @@ describe("Secret Import Router", async () => { }) }) ); - await deleteSecretImport(payload.id); + await deleteSecretImport({ + id: payload.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Get secret imports", async () => { - const createdImport1 = await createSecretImport("/", "prod"); - const createdImport2 = await createSecretImport("/", "staging"); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "prod" + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "staging" + }); const res = await testServer.inject({ method: "GET", url: `/api/v1/secret-imports`, @@ -98,16 +85,42 @@ describe("Secret Import Router", async () => { }) ]) ); - await deleteSecretImport(createdImport1.id); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Update secret import position", async () => { const prodImportDetails = { path: "/", envSlug: "prod" }; const stagingImportDetails = { path: "/", envSlug: "staging" }; - const createdImport1 = await createSecretImport(prodImportDetails.path, prodImportDetails.envSlug); - const createdImport2 = await createSecretImport(stagingImportDetails.path, stagingImportDetails.envSlug); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: prodImportDetails.path, + importEnv: prodImportDetails.envSlug + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: stagingImportDetails.path, + importEnv: stagingImportDetails.envSlug + }); const updateImportRes = await testServer.inject({ method: "PATCH", @@ -161,14 +174,47 @@ describe("Secret Import Router", async () => { expect(secretImportList.secretImports[1].id).toEqual(createdImport1.id); expect(secretImportList.secretImports[0].id).toEqual(createdImport2.id); - await deleteSecretImport(createdImport1.id); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); test("Delete secret import position", async () => { - const createdImport1 = await createSecretImport("/", "prod"); - const createdImport2 = await createSecretImport("/", "staging"); - const deletedImport = await deleteSecretImport(createdImport1.id); + const createdImport1 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "prod" + }); + const createdImport2 = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: "/", + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.project.id, + importPath: "/", + importEnv: "staging" + }); + const deletedImport = await deleteSecretImport({ + id: createdImport1.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); + // check for default environments expect(deletedImport).toEqual( expect.objectContaining({ @@ -201,6 +247,391 @@ describe("Secret Import Router", async () => { expect(secretImportList.secretImports.length).toEqual(1); expect(secretImportList.secretImports[0].position).toEqual(1); - await deleteSecretImport(createdImport2.id); + await deleteSecretImport({ + id: createdImport2.id, + workspaceId: seedData1.project.id, + environmentSlug: seedData1.environment.slug, + secretPath: "/", + authToken: jwtAuthToken + }); }); }); + +// dev <- stage <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret import waterfall pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging" + }); + + const stageImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod" + }); + + return async () => { + await deleteSecretImport({ + id: stageImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check one level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + }); + + test("Check two level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + + expect(secret.secretKey).toBe("PROD_KEY"); + expect(secret.secretValue).toBe("prod-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + } +); + +// dev <- stage, dev <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret import 1-N pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging" + }); + + const devImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod" + }); + + return async () => { + await deleteSecretImport({ + id: devImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }), + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + } +); diff --git a/backend/e2e-test/routes/v1/secret-replication.spec.ts b/backend/e2e-test/routes/v1/secret-replication.spec.ts new file mode 100644 index 000000000..d28223d2c --- /dev/null +++ b/backend/e2e-test/routes/v1/secret-replication.spec.ts @@ -0,0 +1,405 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretImport, deleteSecretImport } from "e2e-test/testUtils/secret-imports"; +import { createSecretV2, deleteSecretV2, getSecretByNameV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + +import { seedData1 } from "@app/db/seed-data"; + +// dev <- stage <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret replication waterfall pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging", + isReplication: true + }); + + const stageImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod", + isReplication: true + }); + + return async () => { + await deleteSecretImport({ + id: stageImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging", + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check one level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + }); + + test("Check two level imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + + expect(secret.secretKey).toBe("PROD_KEY"); + expect(secret.secretValue).toBe("prod-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + }, + { timeout: 30000 } +); + +// dev <- stage, dev <- prod +describe.each([{ path: "/" }, { path: "/deep" }])( + "Secret replication 1-N pattern testing - %path", + ({ path: testSuitePath }) => { + beforeAll(async () => { + let prodFolder: { id: string }; + let stagingFolder: { id: string }; + let devFolder: { id: string }; + + if (testSuitePath !== "/") { + prodFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + stagingFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + + devFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: "/", + name: "deep" + }); + } + + const devImportFromStage = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "staging", + isReplication: true + }); + + const devImportFromProd = await createSecretImport({ + authToken: jwtAuthToken, + secretPath: testSuitePath, + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + importPath: testSuitePath, + importEnv: "prod", + isReplication: true + }); + + return async () => { + await deleteSecretImport({ + id: devImportFromProd.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + await deleteSecretImport({ + id: devImportFromStage.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + + if (prodFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "prod" + }); + } + + if (stagingFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: stagingFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: "staging" + }); + } + + if (devFolder) { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: devFolder.id, + workspaceId: seedData1.projectV3.id, + environmentSlug: seedData1.environment.slug + }); + } + }; + }); + + test("Check imported secret exist", async () => { + await createSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY", + value: "stage-value" + }); + + await createSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY", + value: "prod-value" + }); + + // wait for 5 second for replication to finish + await new Promise((resolve) => { + setTimeout(resolve, 5000); // time to breathe for db + }); + + const secret = await getSecretByNameV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + + expect(secret.secretKey).toBe("STAGING_KEY"); + expect(secret.secretValue).toBe("stage-value"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken + }); + expect(listSecrets.imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "STAGING_KEY", + secretValue: "stage-value" + }) + ]) + }), + expect.objectContaining({ + secrets: expect.arrayContaining([ + expect.objectContaining({ + secretKey: "PROD_KEY", + secretValue: "prod-value" + }) + ]) + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: "staging", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "STAGING_KEY" + }); + await deleteSecretV2({ + environmentSlug: "prod", + workspaceId: seedData1.projectV3.id, + secretPath: testSuitePath, + authToken: jwtAuthToken, + key: "PROD_KEY" + }); + }); + }, + { timeout: 30000 } +); diff --git a/backend/e2e-test/routes/v3/secret-reference.spec.ts b/backend/e2e-test/routes/v3/secret-reference.spec.ts new file mode 100644 index 000000000..256e981ba --- /dev/null +++ b/backend/e2e-test/routes/v3/secret-reference.spec.ts @@ -0,0 +1,97 @@ +import { createFolder, deleteFolder } from "e2e-test/testUtils/folders"; +import { createSecretV2, deleteSecretV2, getSecretsV2 } from "e2e-test/testUtils/secrets"; + +import { seedData1 } from "@app/db/seed-data"; + +describe("Secret expansion", () => { + const projectId = seedData1.projectV3.id; + + beforeAll(async () => { + const prodRootFolder = await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/", + name: "deep" + }); + + await createFolder({ + authToken: jwtAuthToken, + environmentSlug: "prod", + workspaceId: projectId, + secretPath: "/deep", + name: "nested" + }); + + return async () => { + await deleteFolder({ + authToken: jwtAuthToken, + secretPath: "/", + id: prodRootFolder.id, + workspaceId: projectId, + environmentSlug: "prod" + }); + }; + }); + + test("Local secret reference", async () => { + await createSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "HELLO", + value: "world" + }); + + await createSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "TEST", + // eslint-disable-next-line + value: "hello ${HELLO}" + }); + // + // const expandedSecret = await getSecretByNameV2({ + // environmentSlug: seedData1.environment.slug, + // workspaceId: projectId, + // secretPath: "/", + // authToken: jwtAuthToken, + // key: "TEST" + // }); + // expect(expandedSecret.secretValue).toBe("hello world"); + + const listSecrets = await getSecretsV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken + }); + expect(listSecrets.secrets).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + secretKey: "TEST", + secretValue: "hello world" + }) + ]) + ); + + await deleteSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "TEST" + }); + + await deleteSecretV2({ + environmentSlug: seedData1.environment.slug, + workspaceId: projectId, + secretPath: "/", + authToken: jwtAuthToken, + key: "HELLO" + }); + }); +}); diff --git a/backend/e2e-test/routes/v3/secrets-v2.spec.ts b/backend/e2e-test/routes/v3/secrets-v2.spec.ts index 70066c061..dc02587cd 100644 --- a/backend/e2e-test/routes/v3/secrets-v2.spec.ts +++ b/backend/e2e-test/routes/v3/secrets-v2.spec.ts @@ -8,6 +8,7 @@ type TRawSecret = { secretComment?: string; version: number; }; + const createSecret = async (dto: { path: string; key: string; value: string; comment: string; type?: SecretType }) => { const createSecretReqBody = { workspaceId: seedData1.projectV3.id, diff --git a/backend/e2e-test/testUtils/folders.ts b/backend/e2e-test/testUtils/folders.ts new file mode 100644 index 000000000..a5484a22e --- /dev/null +++ b/backend/e2e-test/testUtils/folders.ts @@ -0,0 +1,73 @@ +type TFolder = { + id: string; + name: string; +}; + +export const createFolder = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + name: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + name: dto.name, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder as TFolder; +}; + +export const deleteFolder = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + id: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/folders/${dto.id}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folder as TFolder; +}; + +export const listFolders = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v1/folders`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + expect(res.statusCode).toBe(200); + return res.json().folders as TFolder[]; +}; diff --git a/backend/e2e-test/testUtils/secret-imports.ts b/backend/e2e-test/testUtils/secret-imports.ts new file mode 100644 index 000000000..370a8dfb8 --- /dev/null +++ b/backend/e2e-test/testUtils/secret-imports.ts @@ -0,0 +1,93 @@ +type TSecretImport = { + id: string; + importEnv: { + name: string; + slug: string; + id: string; + }; + importPath: string; +}; + +export const createSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + isReplication?: boolean; + secretPath: string; + importPath: string; + importEnv: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "POST", + url: `/api/v1/secret-imports`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + isReplication: dto.isReplication, + path: dto.secretPath, + import: { + environment: dto.importEnv, + path: dto.importPath + } + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport as TSecretImport; +}; + +export const deleteSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; + id: string; +}) => { + const res = await testServer.inject({ + method: "DELETE", + url: `/api/v1/secret-imports/${dto.id}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImport"); + return payload.secretImport as TSecretImport; +}; + +export const listSecretImport = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const res = await testServer.inject({ + method: "GET", + url: `/api/v1/secret-imports`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + path: dto.secretPath + } + }); + + expect(res.statusCode).toBe(200); + const payload = JSON.parse(res.payload); + expect(payload).toHaveProperty("secretImports"); + return payload.secretImports as TSecretImport[]; +}; diff --git a/backend/e2e-test/testUtils/secrets.ts b/backend/e2e-test/testUtils/secrets.ts new file mode 100644 index 000000000..96ecc91c6 --- /dev/null +++ b/backend/e2e-test/testUtils/secrets.ts @@ -0,0 +1,128 @@ +import { SecretType } from "@app/db/schemas"; + +type TRawSecret = { + secretKey: string; + secretValue: string; + secretComment?: string; + version: number; +}; + +export const createSecretV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + value: string; + comment?: string; + authToken: string; + type?: SecretType; +}) => { + const createSecretReqBody = { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + type: dto.type || SecretType.Shared, + secretPath: dto.secretPath, + secretKey: dto.key, + secretValue: dto.value, + secretComment: dto.comment + }; + const createSecRes = await testServer.inject({ + method: "POST", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: createSecretReqBody + }); + expect(createSecRes.statusCode).toBe(200); + const createdSecretPayload = JSON.parse(createSecRes.payload); + expect(createdSecretPayload).toHaveProperty("secret"); + return createdSecretPayload.secret as TRawSecret; +}; + +export const deleteSecretV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + authToken: string; +}) => { + const deleteSecRes = await testServer.inject({ + method: "DELETE", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + body: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath + } + }); + expect(deleteSecRes.statusCode).toBe(200); + const updatedSecretPayload = JSON.parse(deleteSecRes.payload); + expect(updatedSecretPayload).toHaveProperty("secret"); + return updatedSecretPayload.secret as TRawSecret; +}; + +export const getSecretByNameV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + key: string; + authToken: string; +}) => { + const response = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw/${dto.key}`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath, + expandSecretReferences: "true", + include_imports: "true" + } + }); + expect(response.statusCode).toBe(200); + const payload = JSON.parse(response.payload); + expect(payload).toHaveProperty("secret"); + return payload.secret as TRawSecret; +}; + +export const getSecretsV2 = async (dto: { + workspaceId: string; + environmentSlug: string; + secretPath: string; + authToken: string; +}) => { + const getSecretsResponse = await testServer.inject({ + method: "GET", + url: `/api/v3/secrets/raw`, + headers: { + authorization: `Bearer ${dto.authToken}` + }, + query: { + workspaceId: dto.workspaceId, + environment: dto.environmentSlug, + secretPath: dto.secretPath, + expandSecretReferences: "true", + include_imports: "true" + } + }); + expect(getSecretsResponse.statusCode).toBe(200); + const getSecretsPayload = JSON.parse(getSecretsResponse.payload); + expect(getSecretsPayload).toHaveProperty("secrets"); + expect(getSecretsPayload).toHaveProperty("imports"); + return getSecretsPayload as { + secrets: TRawSecret[]; + imports: { + secretPath: string; + environment: string; + folderId: string; + secrets: TRawSecret[]; + }[]; + }; +}; diff --git a/backend/e2e-test/vitest-environment-knex.ts b/backend/e2e-test/vitest-environment-knex.ts index 37f8e2c48..f57e54373 100644 --- a/backend/e2e-test/vitest-environment-knex.ts +++ b/backend/e2e-test/vitest-environment-knex.ts @@ -11,10 +11,11 @@ import { initLogger } from "@app/lib/logger"; import { main } from "@app/server/app"; import { AuthMethod, AuthTokenType } from "@app/services/auth/auth-type"; -import { mockQueue } from "./mocks/queue"; import { mockSmtpServer } from "./mocks/smtp"; -import { mockKeyStore } from "./mocks/keystore"; import { initDbConnection } from "@app/db"; +import { queueServiceFactory } from "@app/queue"; +import { keyStoreFactory } from "@app/keystore/keystore"; +import { Redis } from "ioredis"; dotenv.config({ path: path.join(__dirname, "../../.env.test"), debug: true }); export default { @@ -28,6 +29,9 @@ export default { dbRootCert: cfg.DB_ROOT_CERT }); + const redis = new Redis(cfg.REDIS_URL); + await redis.flushdb("ASYNC"); + try { await db.migrate.rollback( { @@ -48,8 +52,8 @@ export default { extension: "ts" }); const smtp = mockSmtpServer(); - const queue = mockQueue(); - const keyStore = mockKeyStore(); + const queue = queueServiceFactory(cfg.REDIS_URL); + const keyStore = keyStoreFactory(cfg.REDIS_URL); const server = await main({ db, smtp, logger, queue, keyStore }); // @ts-expect-error type globalThis.testServer = server; @@ -91,6 +95,9 @@ export default { }, true ); + + await redis.flushdb("ASYNC"); + redis.disconnect(); await db.destroy(); } };