From 21a135b9d43ea94ef0e7792a87fcd2862514b56c Mon Sep 17 00:00:00 2001 From: Kamil Kisiela Date: Thu, 5 Jan 2023 11:27:42 +0100 Subject: [PATCH] Fix the default retention period of personal plans in PG (#949) --- integration-tests/.env | 1 + .../docker-compose.integration.yaml | 2 +- integration-tests/jest.config.js | 2 +- integration-tests/package.json | 1 + integration-tests/testkit/seed.ts | 45 ++++++++++++++++ .../tests/api/artifacts-cdn.spec.ts | 2 +- .../tests/api/organization/transfer.spec.ts | 6 +-- integration-tests/tests/api/sign-up.spec.ts | 54 +++++++++++++++++++ integration-tests/tests/cli/schema.spec.ts | 2 +- integration-tests/tsconfig.json | 3 +- packages/services/rate-limit/src/api.ts | 2 +- ...2023.01.04T17.00.23.hobby-7-by-default.sql | 5 ++ ...2023.01.04T17.00.23.hobby-7-by-default.sql | 1 + pnpm-lock.yaml | 2 + 14 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 packages/services/storage/migrations/actions/2023.01.04T17.00.23.hobby-7-by-default.sql create mode 100644 packages/services/storage/migrations/actions/down/2023.01.04T17.00.23.hobby-7-by-default.sql diff --git a/integration-tests/.env b/integration-tests/.env index 619b96a5e..09bf98236 100644 --- a/integration-tests/.env +++ b/integration-tests/.env @@ -20,3 +20,4 @@ RATE_LIMIT_ENDPOINT=http://rate-limit:3009 CLICKHOUSE_ASYNC_INSERT_BUSY_TIMEOUT_MS=500 CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE=1000 EXTERNAL_COMPOSITION_SECRET=secretsecret +LIMIT_CACHE_UPDATE_INTERVAL_MS=2000 diff --git a/integration-tests/docker-compose.integration.yaml b/integration-tests/docker-compose.integration.yaml index b45cc1d1d..a537bd24c 100644 --- a/integration-tests/docker-compose.integration.yaml +++ b/integration-tests/docker-compose.integration.yaml @@ -139,7 +139,7 @@ services: environment: NODE_ENV: production LOG_LEVEL: debug - LIMIT_CACHE_UPDATE_INTERVAL_MS: 2000 + LIMIT_CACHE_UPDATE_INTERVAL_MS: '${LIMIT_CACHE_UPDATE_INTERVAL_MS}' POSTGRES_HOST: db POSTGRES_PORT: 5432 POSTGRES_DB: '${POSTGRES_DB}' diff --git a/integration-tests/jest.config.js b/integration-tests/jest.config.js index b1ca56754..4c0ec3ce3 100644 --- a/integration-tests/jest.config.js +++ b/integration-tests/jest.config.js @@ -20,7 +20,7 @@ export default { }), '^(\\.{1,2}/.*)\\.js$': '$1', }, - testTimeout: 200000, + testTimeout: 30_000, setupFilesAfterEnv: ['dotenv/config'], collectCoverage: false, }; diff --git a/integration-tests/package.json b/integration-tests/package.json index fca58152b..fd019f2cb 100644 --- a/integration-tests/package.json +++ b/integration-tests/package.json @@ -28,6 +28,7 @@ "zod": "3.20.2" }, "devDependencies": { + "@hive/rate-limit": "workspace:*", "@hive/server": "workspace:*", "@types/dockerode": "3.3.14", "@types/ioredis": "4.28.10", diff --git a/integration-tests/testkit/seed.ts b/integration-tests/testkit/seed.ts index d1560134b..76d362694 100644 --- a/integration-tests/testkit/seed.ts +++ b/integration-tests/testkit/seed.ts @@ -1,5 +1,7 @@ +import { gql } from '@app/gql'; import { OrganizationAccessScope, + OrganizationType, ProjectAccessScope, ProjectType, TargetAccessScope, @@ -31,6 +33,7 @@ import { updateMemberAccess, updateSchemaVersionStatus, } from './flow'; +import { execute } from './graphql'; import { collect, CollectedOperation } from './usage'; import { generateUnique } from './utils'; @@ -45,6 +48,48 @@ export function initSeed() { return { ownerEmail, ownerToken, + async createPersonalProject(projectType: ProjectType) { + const orgs = await execute({ + document: gql(/* GraphQL */ ` + query myOrganizations { + organizations { + total + nodes { + id + cleanId + name + type + } + } + } + `), + authToken: ownerToken, + }).then(r => r.expectNoGraphQLErrors()); + + const personalOrg = orgs.organizations.nodes.find( + o => o.type === OrganizationType.Personal, + ); + + if (!personalOrg) { + throw new Error('Personal organization should exist'); + } + + const projectResult = await createProject( + { + organization: personalOrg.cleanId, + type: projectType, + name: generateUnique(), + }, + ownerToken, + ).then(r => r.expectNoGraphQLErrors()); + + const targets = projectResult.createProject.ok!.createdTargets; + const target = targets[0]; + + return { + target, + }; + }, async createOrg() { const orgName = generateUnique(); const orgResult = await createOrganization({ name: orgName }, ownerToken).then(r => diff --git a/integration-tests/tests/api/artifacts-cdn.spec.ts b/integration-tests/tests/api/artifacts-cdn.spec.ts index ff6ca9558..06de84ea3 100644 --- a/integration-tests/tests/api/artifacts-cdn.spec.ts +++ b/integration-tests/tests/api/artifacts-cdn.spec.ts @@ -1,3 +1,4 @@ +import { ProjectType, TargetAccessScope } from '@app/gql/graphql'; import { DeleteObjectsCommand, GetObjectCommand, @@ -5,7 +6,6 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { fetch } from '@whatwg-node/fetch'; -import { ProjectType, TargetAccessScope } from '../../testkit/gql/graphql'; import { initSeed } from '../../testkit/seed'; import { getServiceHost } from '../../testkit/utils'; diff --git a/integration-tests/tests/api/organization/transfer.spec.ts b/integration-tests/tests/api/organization/transfer.spec.ts index 1b05db8d7..66db4ce7f 100644 --- a/integration-tests/tests/api/organization/transfer.spec.ts +++ b/integration-tests/tests/api/organization/transfer.spec.ts @@ -1,13 +1,9 @@ +import { OrganizationAccessScope, ProjectAccessScope, TargetAccessScope } from '@app/gql/graphql'; import { answerOrganizationTransferRequest, getOrganizationTransferRequest, requestOrganizationTransfer, } from '../../../testkit/flow'; -import { - OrganizationAccessScope, - ProjectAccessScope, - TargetAccessScope, -} from '../../../testkit/gql/graphql'; import { initSeed } from '../../../testkit/seed'; test.concurrent( diff --git a/integration-tests/tests/api/sign-up.spec.ts b/integration-tests/tests/api/sign-up.spec.ts index 1d183c9c7..33c84708b 100644 --- a/integration-tests/tests/api/sign-up.spec.ts +++ b/integration-tests/tests/api/sign-up.spec.ts @@ -1,6 +1,17 @@ import { gql } from '@app/gql'; +import { ProjectType } from '@app/gql/graphql'; +import type { RateLimitApi } from '@hive/rate-limit'; +import { createTRPCProxyClient, httpLink } from '@trpc/client'; +import { createFetch } from '@whatwg-node/fetch'; +import { ensureEnv } from '../../testkit/env'; +import { waitFor } from '../../testkit/flow'; import { execute } from '../../testkit/graphql'; import { initSeed } from '../../testkit/seed'; +import { getServiceHost } from '../../testkit/utils'; + +const { fetch } = createFetch({ + useNodeFetch: true, +}); test.concurrent('should auto-create an organization for freshly signed-up user', async () => { const { ownerToken } = await initSeed().createOwner(); @@ -22,6 +33,49 @@ test.concurrent('should auto-create an organization for freshly signed-up user', expect(result.organizations.total).toBe(1); }); +test.concurrent( + 'freshly signed-up user should have a Hobby plan with 7 days of retention', + async () => { + const { ownerToken, createPersonalProject } = await initSeed().createOwner(); + const result = await execute({ + document: gql(/* GraphQL */ ` + query organizations { + organizations { + total + nodes { + id + name + } + } + } + `), + authToken: ownerToken, + }).then(r => r.expectNoGraphQLErrors()); + + expect(result.organizations.total).toBe(1); + + const { target } = await createPersonalProject(ProjectType.Single); + + await waitFor(ensureEnv('LIMIT_CACHE_UPDATE_INTERVAL_MS', 'number') + 1_000); // wait for rate-limit to update + + const rateLimit = createTRPCProxyClient({ + links: [ + httpLink({ + url: `http://${await getServiceHost('rate-limit', 3009)}/trpc`, + fetch, + }), + ], + }); + + // Expect the default retention for a Hobby plan to be 7 days + await expect( + rateLimit.getRetention.query({ + targetId: target.id, + }), + ).resolves.toEqual(7); + }, +); + test.concurrent( 'should auto-create an organization for freshly signed-up user with no race-conditions', async () => { diff --git a/integration-tests/tests/cli/schema.spec.ts b/integration-tests/tests/cli/schema.spec.ts index abd58652a..a2bee8ecb 100644 --- a/integration-tests/tests/cli/schema.spec.ts +++ b/integration-tests/tests/cli/schema.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable no-process-env */ import { createHash } from 'node:crypto'; +import { ProjectType } from '@app/gql/graphql'; import { schemaCheck, schemaPublish } from '../../testkit/cli'; -import { ProjectType } from '../../testkit/gql/graphql'; import { initSeed } from '../../testkit/seed'; test.concurrent('can publish and check a schema with target:registry:read access', async () => { diff --git a/integration-tests/tsconfig.json b/integration-tests/tsconfig.json index 9346d6e8a..9f2f2fc7a 100644 --- a/integration-tests/tsconfig.json +++ b/integration-tests/tsconfig.json @@ -6,7 +6,8 @@ "esModuleInterop": true, "paths": { "@hive/server": ["../packages/services/server/src/api.ts"], - "@hive/storage": ["../packages/services/storage/src/index.ts"] + "@hive/storage": ["../packages/services/storage/src/index.ts"], + "@hive/rate-limit": ["../packages/services/rate-limit/src/api.ts"] } }, "include": ["testkit", "tests"] diff --git a/packages/services/rate-limit/src/api.ts b/packages/services/rate-limit/src/api.ts index a3d245241..551dd98fc 100644 --- a/packages/services/rate-limit/src/api.ts +++ b/packages/services/rate-limit/src/api.ts @@ -9,7 +9,7 @@ export type RateLimitInput = z.infer; const VALIDATION = z .object({ - id: z.string().nonempty(), + id: z.string().min(1), entityType: z.enum(['organization', 'target']), type: z.enum(['operations-reporting']), /** diff --git a/packages/services/storage/migrations/actions/2023.01.04T17.00.23.hobby-7-by-default.sql b/packages/services/storage/migrations/actions/2023.01.04T17.00.23.hobby-7-by-default.sql new file mode 100644 index 000000000..644305b1f --- /dev/null +++ b/packages/services/storage/migrations/actions/2023.01.04T17.00.23.hobby-7-by-default.sql @@ -0,0 +1,5 @@ +-- Update Hobby with 3d to 7d - personal orgs were created with the default value of 3d +UPDATE public.organizations SET limit_retention_days = 7 WHERE plan_name = 'HOBBY' AND limit_retention_days = 3; + +-- Update limit_retention_days default value to 7 +ALTER table public.organizations ALTER COLUMN limit_retention_days SET DEFAULT 7; \ No newline at end of file diff --git a/packages/services/storage/migrations/actions/down/2023.01.04T17.00.23.hobby-7-by-default.sql b/packages/services/storage/migrations/actions/down/2023.01.04T17.00.23.hobby-7-by-default.sql new file mode 100644 index 000000000..b0bb885a8 --- /dev/null +++ b/packages/services/storage/migrations/actions/down/2023.01.04T17.00.23.hobby-7-by-default.sql @@ -0,0 +1 @@ +-- no need \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81733d347..6b5dee62b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,7 @@ importers: '@esm2cjs/execa': 6.1.1-cjs.1 '@graphql-hive/core': 0.2.3 '@graphql-typed-document-node/core': 3.1.1 + '@hive/rate-limit': workspace:* '@hive/server': workspace:* '@trpc/client': 10.7.0 '@trpc/server': 10.7.0 @@ -196,6 +197,7 @@ importers: slonik: 30.1.2_wg2hxbo7txnklmvja4aeqnygfi zod: 3.20.2 devDependencies: + '@hive/rate-limit': link:../packages/services/rate-limit '@hive/server': link:../packages/services/server '@types/dockerode': 3.3.14 '@types/ioredis': 4.28.10