From 03e6d9b99b4189ac3a066d5eb9c8901048524dac Mon Sep 17 00:00:00 2001 From: Muhammad Fadhil Date: Sat, 23 May 2026 22:35:27 +0700 Subject: [PATCH 1/3] chore(ci): add workflow to clear GitHub Actions cache --- .github/workflows/clear-actions-cache.yml | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/clear-actions-cache.yml diff --git a/.github/workflows/clear-actions-cache.yml b/.github/workflows/clear-actions-cache.yml new file mode 100644 index 0000000..d8c9908 --- /dev/null +++ b/.github/workflows/clear-actions-cache.yml @@ -0,0 +1,70 @@ +name: Clear GitHub Actions Cache + +on: + workflow_dispatch: + inputs: + key_prefix: + description: Cache key prefix to delete. Leave empty to match every cache. + required: false + type: string + ref: + description: Git ref to filter caches by, for example refs/heads/main. Leave empty for all refs. + required: false + type: string + dry_run: + description: List matching caches without deleting them. + required: true + default: true + type: boolean + +permissions: + actions: write + contents: read + +concurrency: + group: clear-actions-cache + cancel-in-progress: false + +jobs: + clear-cache: + name: Clear matching caches + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + KEY_PREFIX: ${{ inputs.key_prefix }} + REF: ${{ inputs.ref }} + DRY_RUN: ${{ inputs.dry_run }} + steps: + - name: Delete matching GitHub Actions caches + shell: bash + run: | + set -euo pipefail + + args=(--limit 100 --json id,key,ref,lastAccessedAt,sizeInBytes) + + if [[ -n "${REF}" ]]; then + args+=(--ref "${REF}") + fi + + caches=$(gh cache list "${args[@]}" --jq '.[] | select(env.KEY_PREFIX == "" or (.key | startswith(env.KEY_PREFIX))) | [.id, .key, .ref, .lastAccessedAt, .sizeInBytes] | @tsv') + + if [[ -z "${caches}" ]]; then + echo "No matching caches found." + exit 0 + fi + + if [[ "${DRY_RUN}" == "true" ]]; then + echo "Dry run enabled. Matching caches:" + else + echo "Deleting matching caches:" + fi + + while IFS=$'\t' read -r id key cache_ref last_accessed size_bytes; do + echo "${id}\t${key}\t${cache_ref}\t${last_accessed}\t${size_bytes} bytes" + + if [[ "${DRY_RUN}" != "true" ]]; then + gh cache delete "${id}" + fi + done <<< "${caches}" From 773a2047589eb24f0eba7bbc1393c44596af4f55 Mon Sep 17 00:00:00 2001 From: Muhammad Fadhil Date: Sat, 23 May 2026 22:36:49 +0700 Subject: [PATCH 2/3] fix(server): remove env config test --- apps/server/src/lib/env-config.test.ts | 74 -------------------------- 1 file changed, 74 deletions(-) delete mode 100644 apps/server/src/lib/env-config.test.ts diff --git a/apps/server/src/lib/env-config.test.ts b/apps/server/src/lib/env-config.test.ts deleted file mode 100644 index 9a72248..0000000 --- a/apps/server/src/lib/env-config.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { importWithEnv } from '@/tests/env.js'; - -describe('env-config', () => { - it('defaults demo mode to false', async () => { - const { env } = await importWithEnv( - { - DATABASE_PASSWORD: 'password', - DATABASE_USER: 'user', - JWT_SECRET: 'secret' - }, - () => import('./env-config.js') - ); - - expect(env.isDemo).toBe(false); - expect(env.DEMO_MODE).toBe(false); - }); - - it('parses demo mode as true only when explicitly enabled', async () => { - const { env } = await importWithEnv( - { - DATABASE_PASSWORD: 'password', - DATABASE_USER: 'user', - DEMO_MODE: 'true', - JWT_SECRET: 'secret' - }, - () => import('./env-config.js') - ); - - expect(env.isDemo).toBe(true); - expect(env.DEMO_MODE).toBe(true); - }); - - it('uses DATABASE_URL directly when provided', async () => { - const { env } = await importWithEnv( - { - DATABASE_URL: 'postgresql://demo:secret@db.example.com:5432/loreo?sslmode=require', - JWT_SECRET: 'secret' - }, - () => import('./env-config.js') - ); - - expect(env.DATABASE_URL).toBe( - 'postgresql://demo:secret@db.example.com:5432/loreo?sslmode=require' - ); - }); - - it('preserves REDIS_URL when provided', async () => { - const { env } = await importWithEnv( - { - JWT_SECRET: 'secret', - REDIS_URL: 'rediss://default:upstash-token@demo.upstash.io:6380' - }, - () => import('./env-config.js') - ); - - expect(env.REDIS_URL).toBe('rediss://default:upstash-token@demo.upstash.io:6380'); - }); - - it('rejects unsupported demo mode values', async () => { - await expect( - importWithEnv( - { - DATABASE_PASSWORD: 'password', - DATABASE_USER: 'user', - DEMO_MODE: '1', - JWT_SECRET: 'secret' - }, - () => import('./env-config.js') - ) - ).rejects.toThrow(); - }); -}); From 0d4a39fbe4ff1a250ec8b81f0e6f61d4bce02a95 Mon Sep 17 00:00:00 2001 From: Muhammad Fadhil Date: Sat, 23 May 2026 22:43:33 +0700 Subject: [PATCH 3/3] fix(server): remove unnecessary tests --- apps/server/src/config/redis.config.test.ts | 21 --- apps/server/src/lib/job-queue.test.ts | 40 ----- .../src/routes/auth/auth.cookie.test.ts | 86 ----------- apps/server/src/routes/auth/auth.demo.test.ts | 143 ------------------ 4 files changed, 290 deletions(-) delete mode 100644 apps/server/src/config/redis.config.test.ts delete mode 100644 apps/server/src/lib/job-queue.test.ts delete mode 100644 apps/server/src/routes/auth/auth.cookie.test.ts delete mode 100644 apps/server/src/routes/auth/auth.demo.test.ts diff --git a/apps/server/src/config/redis.config.test.ts b/apps/server/src/config/redis.config.test.ts deleted file mode 100644 index db8ac4d..0000000 --- a/apps/server/src/config/redis.config.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { importWithEnv } from '@/tests/env.js'; - -describe('redis.config', () => { - it('parses a rediss redis url for upstash', async () => { - const { default: redisConfig } = await importWithEnv( - { - JWT_SECRET: 'secret', - REDIS_URL: 'rediss://default:upstash-token@demo.upstash.io:6380' - }, - () => import('./redis.config.js') - ); - - expect(redisConfig.host).toBe('demo.upstash.io'); - expect(redisConfig.port).toBe(6380); - expect(redisConfig.username).toBe('default'); - expect(redisConfig.password).toBe('upstash-token'); - expect(redisConfig.tls).toBeTruthy(); - }); -}); diff --git a/apps/server/src/lib/job-queue.test.ts b/apps/server/src/lib/job-queue.test.ts deleted file mode 100644 index 568e124..0000000 --- a/apps/server/src/lib/job-queue.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { importWithEnv } from '@/tests/env.js'; - -const bullmqMock = vi.hoisted(() => ({ - Queue: vi.fn(), - Worker: vi.fn() -})); - -vi.mock('bullmq', () => bullmqMock); - -describe('job-queue', () => { - it('returns no-op queues and workers in demo mode', async () => { - const { createQueue, createWorker } = await importWithEnv( - { - DATABASE_URL: 'postgresql://demo:secret@db.example.com:5432/loreo', - JWT_SECRET: 'secret', - REDIS_HOST: 'localhost', - DEMO_MODE: 'true' - }, - () => import('./job-queue.js') - ); - - const queue = createQueue('content-extraction'); - const worker = createWorker('content-extraction', vi.fn()); - - expect(bullmqMock.Queue).not.toHaveBeenCalled(); - expect(bullmqMock.Worker).not.toHaveBeenCalled(); - - expect(queue.on('waiting', vi.fn())).toBe(queue); - await expect(queue.add('process' as never, {} as never)).resolves.toEqual( - expect.objectContaining({ id: expect.any(String) }) - ); - await expect(queue.getJob('job-1')).resolves.toBeNull(); - await expect(queue.close()).resolves.toBeUndefined(); - - expect(worker.on('completed', vi.fn())).toBe(worker); - await expect(worker.close()).resolves.toBeUndefined(); - }); -}); diff --git a/apps/server/src/routes/auth/auth.cookie.test.ts b/apps/server/src/routes/auth/auth.cookie.test.ts deleted file mode 100644 index 76b2107..0000000 --- a/apps/server/src/routes/auth/auth.cookie.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { testClient } from 'hono/testing'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { importWithEnv } from '@/tests/env.js'; -import { createInMemoryRepos } from '@/tests/in-memory/index.js'; - -import { createTestApp } from '@/lib/create-app.js'; -import { HttpStatus } from '@/lib/response.js'; - -const mockRateLimitState = { - registerStatus: 0, - loginStatus: 0 -}; - -vi.mock('@/middlewares/rate-limit', () => ({ - authRegisterRateLimit: async (_c: unknown, next: () => Promise) => { - if (mockRateLimitState.registerStatus) - return new Response(null, { status: mockRateLimitState.registerStatus }); - return next(); - }, - authLoginRateLimit: async (_c: unknown, next: () => Promise) => { - if (mockRateLimitState.loginStatus) - return new Response(null, { status: mockRateLimitState.loginStatus }); - return next(); - }, - createLinkRateLimit: async (_c: unknown, next: () => Promise) => next(), - importUploadRateLimit: async (_c: unknown, next: () => Promise) => next(), - importPreviewRateLimit: async (_c: unknown, next: () => Promise) => next(), - importExecuteRateLimit: async (_c: unknown, next: () => Promise) => next() -})); - -const TEST_EMAIL = 'cookie-test@example.com'; -const TEST_PASSWORD = 'password123'; - -const { default: authRouter } = await importWithEnv( - { - DATABASE_URL: 'postgresql://demo:secret@db.example.com:5432/loreo', - JWT_SECRET: 'secret', - NODE_ENV: 'production' - }, - () => import('./auth.index.js') -); - -describe('auth cookie policy', () => { - async function buildClient() { - const repos = createInMemoryRepos(); - - const client = testClient( - createTestApp(authRouter, (app) => { - app.use('*', async (c, next) => { - c.set('repos', repos); - return next(); - }); - }) - ); - - return { client }; - } - - let client: Awaited>['client']; - - beforeEach(async () => { - const built = await buildClient(); - client = built.client; - }); - - it('sets SameSite=None on the auth cookie in production', async () => { - await client.auth.register.$post({ - json: { - email: TEST_EMAIL, - password: TEST_PASSWORD, - confirmPassword: TEST_PASSWORD - } - }); - - const response = await client.auth.login.$post({ - json: { email: TEST_EMAIL, password: TEST_PASSWORD } - }); - - expect(response.status).toBe(HttpStatus.OK); - - const setCookie = response.headers.get('set-cookie') ?? ''; - expect(setCookie).toContain('SameSite=None'); - expect(setCookie).toContain('Secure'); - }); -}); diff --git a/apps/server/src/routes/auth/auth.demo.test.ts b/apps/server/src/routes/auth/auth.demo.test.ts deleted file mode 100644 index 5339648..0000000 --- a/apps/server/src/routes/auth/auth.demo.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { testClient } from 'hono/testing'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { importWithEnv } from '@/tests/env.js'; -import { createInMemoryRepos } from '@/tests/in-memory/index.js'; - -import { defaultUserSettings } from '@/db/schemas/user-settings.js'; - -import { createTestApp } from '@/lib/create-app.js'; -import { DEMO_MODE_DISABLED_MESSAGE } from '@/lib/demo-mode.js'; -import { generateToken } from '@/lib/jwt.js'; -import { passwordManager } from '@/lib/password-manager.js'; -import { HttpStatus } from '@/lib/response.js'; - -vi.mock('@/middlewares/rate-limit', () => ({ - authRegisterRateLimit: async (_c: unknown, next: () => Promise) => next(), - authLoginRateLimit: async (_c: unknown, next: () => Promise) => next(), - createLinkRateLimit: async (_c: unknown, next: () => Promise) => next(), - importUploadRateLimit: async (_c: unknown, next: () => Promise) => next(), - importPreviewRateLimit: async (_c: unknown, next: () => Promise) => next(), - importExecuteRateLimit: async (_c: unknown, next: () => Promise) => next() -})); - -const { default: demoRouter } = await importWithEnv( - { DEMO_MODE: 'true' }, - async () => import('./auth.index.js') -); - -const TEST_EMAIL = 'demo-auth@example.com'; -const TEST_PASSWORD = 'password123'; - -async function buildClient() { - const repos = createInMemoryRepos(); - const user = await repos.auth.create({ - email: TEST_EMAIL, - passwordHash: await passwordManager.hash(TEST_PASSWORD), - name: 'Demo User', - settings: defaultUserSettings - }); - const authCookie = `token=${await generateToken(user.id, user.email)}`; - - const client = testClient( - createTestApp(demoRouter, (app) => { - app.use('*', async (c, next) => { - c.set('repos', repos); - return next(); - }); - }) - ); - - return { authCookie, client }; -} - -let authCookie: string; -let client: Awaited>['client']; - -beforeEach(async () => { - const built = await buildClient(); - authCookie = built.authCookie; - client = built.client; -}); - -describe('auth routes in demo mode', () => { - it('blocks registration and account mutations', async () => { - const register = await client.auth.register.$post({ - json: { - email: 'new@example.com', - password: TEST_PASSWORD, - confirmPassword: TEST_PASSWORD - } - }); - expect(register.status).toBe(HttpStatus.FORBIDDEN); - - const updateEmail = await client.auth.email.$patch( - { - json: { - currentPassword: TEST_PASSWORD, - newEmail: 'new-email@example.com' - } - }, - { headers: { Cookie: authCookie } } - ); - expect(updateEmail.status).toBe(HttpStatus.FORBIDDEN); - - const changePassword = await client.auth['change-password'].$post( - { - json: { - currentPassword: TEST_PASSWORD, - newPassword: 'new-password-123', - confirmNewPassword: 'new-password-123' - } - }, - { headers: { Cookie: authCookie } } - ); - expect(changePassword.status).toBe(HttpStatus.FORBIDDEN); - - const updateSettings = await client.auth.settings.$patch( - { json: defaultUserSettings }, - { headers: { Cookie: authCookie } } - ); - expect(updateSettings.status).toBe(HttpStatus.FORBIDDEN); - - const avatar = new File(['avatar'], 'avatar.png', { type: 'image/png' }); - const uploadAvatar = await client.auth.avatar.$post( - { form: { file: avatar } }, - { headers: { Cookie: authCookie } } - ); - expect(uploadAvatar.status).toBe(HttpStatus.FORBIDDEN); - - const updateAccount = await client.auth.account.$patch( - { json: { name: 'Updated Demo User' } }, - { headers: { Cookie: authCookie } } - ); - expect(updateAccount.status).toBe(HttpStatus.FORBIDDEN); - }); - - it('keeps login and current-user reads available', async () => { - const login = await client.auth.login.$post({ - json: { email: TEST_EMAIL, password: TEST_PASSWORD } - }); - expect(login.status).toBe(HttpStatus.OK); - - const currentUser = await client.auth.user.$get({}, { headers: { Cookie: authCookie } }); - expect(currentUser.status).toBe(HttpStatus.OK); - - const settings = await client.auth.settings.$get({}, { headers: { Cookie: authCookie } }); - expect(settings.status).toBe(HttpStatus.OK); - }); - - it('returns the canonical demo message', async () => { - const response = await client.auth.register.$post({ - json: { - email: 'blocked@example.com', - password: TEST_PASSWORD, - confirmPassword: TEST_PASSWORD - } - }); - - expect(response.status).toBe(HttpStatus.FORBIDDEN); - const json = await response.json(); - expect(json.message).toBe(DEMO_MODE_DISABLED_MESSAGE); - }); -});