From ad1c9113513dbafb55cb6b69b6418dcadd1c82f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:32:47 +0000 Subject: [PATCH 1/3] Initial plan From 0abebb09bb054bd8c2059de84152424ee29a0a4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:46:17 +0000 Subject: [PATCH 2/3] feat: add hotcrm submodule and server integration test infrastructure - Add objectstack-ai/hotcrm as git submodule at server/hotcrm/ - Create shell scripts for starting/stopping/waiting for the server - Create integration test files (auth, CRUD, metadata) with helpers - Add separate jest.integration.config.js for server integration tests - Add integration test CI workflow (.github/workflows/integration.yml) - Add test:integration:server and server management scripts to package.json - Update .gitignore for submodule artifacts and server runtime files - Exclude hotcrm submodule and integration tests from regular jest run - Install ts-jest for the integration test runner Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .github/workflows/integration.yml | 64 ++++++ .gitignore | 12 ++ .gitmodules | 3 + .../server/auth.integration.test.ts | 131 ++++++++++++ .../server/crud.integration.test.ts | 196 ++++++++++++++++++ __tests__/integration/server/helpers.ts | 99 +++++++++ .../server/metadata.integration.test.ts | 114 ++++++++++ jest.config.js | 5 + jest.integration.config.js | 33 +++ package.json | 5 + pnpm-lock.yaml | 106 ++++++++++ scripts/start-integration-server.sh | 46 ++++ scripts/stop-integration-server.sh | 38 ++++ scripts/wait-for-server.sh | 30 +++ server/hotcrm | 1 + 15 files changed, 883 insertions(+) create mode 100644 .github/workflows/integration.yml create mode 100644 .gitmodules create mode 100644 __tests__/integration/server/auth.integration.test.ts create mode 100644 __tests__/integration/server/crud.integration.test.ts create mode 100644 __tests__/integration/server/helpers.ts create mode 100644 __tests__/integration/server/metadata.integration.test.ts create mode 100644 jest.integration.config.js create mode 100755 scripts/start-integration-server.sh create mode 100755 scripts/stop-integration-server.sh create mode 100755 scripts/wait-for-server.sh create mode 160000 server/hotcrm diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..5c525d4 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,64 @@ +name: Integration Tests + +on: + pull_request: + branches: [main, develop] + push: + branches: [main, develop] + # Allow manual trigger + workflow_dispatch: + +permissions: + contents: read + +jobs: + integration: + name: Server Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + # Install mobile app dependencies + - name: Install dependencies + run: pnpm install + + # Install HotCRM submodule dependencies and build + - name: Setup HotCRM server + run: | + cd server/hotcrm + pnpm install + pnpm build + + # Start the server in background + - name: Start HotCRM server + run: ./scripts/start-integration-server.sh --bg + env: + PORT: 4000 + + # Wait for server readiness + - name: Wait for server + run: ./scripts/wait-for-server.sh http://localhost:4000/api/v1/auth/get-session 60 + + # Run integration tests + - name: Run integration tests + run: pnpm test:integration:server + env: + INTEGRATION_SERVER_URL: http://localhost:4000 + + # Stop server + - name: Stop HotCRM server + if: always() + run: ./scripts/stop-integration-server.sh diff --git a/.gitignore b/.gitignore index 3d52bd5..e7f37e0 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,15 @@ coverage/ apps/docs/.next/ apps/docs/.source/ apps/docs/node_modules/ + +# hotcrm submodule build artifacts +server/hotcrm/node_modules/ +server/hotcrm/.pnpm-store/ +server/hotcrm/packages/*/dist/ +server/hotcrm/packages/*/node_modules/ +server/hotcrm/apps/*/node_modules/ +server/hotcrm/apps/*/.next/ + +# integration server runtime files +.hotcrm-server.pid +.hotcrm-server.log diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce4d458 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "server/hotcrm"] + path = server/hotcrm + url = https://github.com/objectstack-ai/hotcrm.git diff --git a/__tests__/integration/server/auth.integration.test.ts b/__tests__/integration/server/auth.integration.test.ts new file mode 100644 index 0000000..92cd9c5 --- /dev/null +++ b/__tests__/integration/server/auth.integration.test.ts @@ -0,0 +1,131 @@ +/** + * Server Integration Tests — Authentication Flow + * + * These tests run against a real HotCRM server and validate the complete + * authentication lifecycle: registration → login → session → logout. + * + * Prerequisites: + * 1. HotCRM server running: `./scripts/start-integration-server.sh --bg` + * 2. Server is ready: `./scripts/wait-for-server.sh` + * + * Run: + * pnpm test:integration:server + */ + +import { api, uniqueEmail, BASE_URL } from "./helpers"; + +const TEST_PASSWORD = "IntTest_Passw0rd!"; + +describe("Authentication Flow", () => { + const email = uniqueEmail(); + let sessionCookie = ""; + + it("should register a new user via /api/v1/auth/sign-up/email", async () => { + const res = await api("/api/v1/auth/sign-up/email", { + method: "POST", + body: JSON.stringify({ + email, + password: TEST_PASSWORD, + name: "Integration Tester", + }), + }); + + expect(res.status).toBeLessThan(400); + + const body = await res.json(); + // The response should contain user data (email or user object) + expect(body).toBeDefined(); + + // Capture session cookie for later requests + sessionCookie = res.headers.get("set-cookie") ?? ""; + }); + + it("should login with the registered credentials via /api/v1/auth/sign-in/email", async () => { + const res = await api("/api/v1/auth/sign-in/email", { + method: "POST", + body: JSON.stringify({ + email, + password: TEST_PASSWORD, + }), + }); + + expect(res.status).toBeLessThan(400); + + const body = await res.json(); + expect(body).toBeDefined(); + + // Update session cookie + const newCookie = res.headers.get("set-cookie"); + if (newCookie) { + sessionCookie = newCookie; + } + }); + + it("should retrieve the current session via /api/v1/auth/get-session", async () => { + const res = await api("/api/v1/auth/get-session", { + headers: sessionCookie ? { Cookie: sessionCookie } : {}, + }); + + expect(res.status).toBeLessThan(400); + + const body = await res.json(); + expect(body).toBeDefined(); + }); + + it("should sign out via /api/v1/auth/sign-out", async () => { + const res = await api("/api/v1/auth/sign-out", { + method: "POST", + headers: sessionCookie ? { Cookie: sessionCookie } : {}, + }); + + expect(res.status).toBeLessThan(400); + }); + + it("should reject login with wrong password", async () => { + const res = await api("/api/v1/auth/sign-in/email", { + method: "POST", + body: JSON.stringify({ + email, + password: "WrongPassword123!", + }), + }); + + // Should fail authentication + expect(res.status).toBeGreaterThanOrEqual(400); + }); +}); + +describe("Registration Validation", () => { + it("should reject registration without email", async () => { + const res = await api("/api/v1/auth/sign-up/email", { + method: "POST", + body: JSON.stringify({ + password: TEST_PASSWORD, + name: "No Email User", + }), + }); + + expect(res.status).toBeGreaterThanOrEqual(400); + }); + + it("should reject registration without password", async () => { + const res = await api("/api/v1/auth/sign-up/email", { + method: "POST", + body: JSON.stringify({ + email: uniqueEmail(), + name: "No Password User", + }), + }); + + expect(res.status).toBeGreaterThanOrEqual(400); + }); +}); + +describe("Server Health", () => { + it("should respond to requests on the base URL", async () => { + // A basic connectivity check — the server should respond + const res = await fetch(BASE_URL); + // Even a 404 is fine — the server is alive + expect(res.status).toBeDefined(); + }); +}); diff --git a/__tests__/integration/server/crud.integration.test.ts b/__tests__/integration/server/crud.integration.test.ts new file mode 100644 index 0000000..fa04cd9 --- /dev/null +++ b/__tests__/integration/server/crud.integration.test.ts @@ -0,0 +1,196 @@ +/** + * Server Integration Tests — CRUD Operations + * + * These tests run against a real HotCRM server and validate data operations + * through the ObjectStack REST API: create, read, update, delete records. + * + * Prerequisites: + * 1. HotCRM server running: `./scripts/start-integration-server.sh --bg` + * 2. Server is ready: `./scripts/wait-for-server.sh` + * + * Run: + * pnpm test:integration:server + */ + +import { api, apiOk, registerAndLogin } from "./helpers"; + +describe("CRUD Operations", () => { + let cookie = ""; + + beforeAll(async () => { + // Register and login to get an authenticated session + const auth = await registerAndLogin(); + cookie = auth.cookie; + }); + + const authed = (init?: RequestInit): RequestInit => ({ + ...init, + headers: { + ...init?.headers, + ...(cookie ? { Cookie: cookie } : {}), + }, + }); + + describe("Account object", () => { + let accountId: string; + + it("should create an account", async () => { + const res = await api( + "/api/v1/account", + authed({ + method: "POST", + body: JSON.stringify({ + name: "Integration Test Corp", + industry: "Technology", + website: "https://integration-test.example.com", + }), + }), + ); + + // Accept 2xx (created) or 4xx if the object doesn't exist yet in this config + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + accountId = body.id ?? body._id ?? body.data?.id; + } else { + // 404 = object not registered, which is acceptable in a minimal server + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should list accounts", async () => { + const res = await api("/api/v1/account", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + // Could be { records: [...] } or { data: [...] } or an array + const records = body.records ?? body.data ?? body; + expect(Array.isArray(records) || typeof records === "object").toBe( + true, + ); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should retrieve a single account by ID", async () => { + if (!accountId) return; // skip if create didn't produce an ID + + const res = await api(`/api/v1/account/${accountId}`, authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should update an account", async () => { + if (!accountId) return; + + const res = await api( + `/api/v1/account/${accountId}`, + authed({ + method: "PUT", + body: JSON.stringify({ + name: "Integration Test Corp (Updated)", + }), + }), + ); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should delete an account", async () => { + if (!accountId) return; + + const res = await api( + `/api/v1/account/${accountId}`, + authed({ method: "DELETE" }), + ); + + if (res.ok) { + // Could return 200 or 204 + expect(res.status).toBeLessThan(300); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); + + describe("Contact object", () => { + it("should create a contact", async () => { + const res = await api( + "/api/v1/contact", + authed({ + method: "POST", + body: JSON.stringify({ + first_name: "Integration", + last_name: "Tester", + email: "contact@integration-test.example.com", + }), + }), + ); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should list contacts", async () => { + const res = await api("/api/v1/contact", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); + + describe("Lead object", () => { + it("should create a lead", async () => { + const res = await api( + "/api/v1/lead", + authed({ + method: "POST", + body: JSON.stringify({ + first_name: "Lead", + last_name: "Prospect", + company: "Test Leads Inc", + status: "New", + }), + }), + ); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should list leads", async () => { + const res = await api("/api/v1/lead", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); +}); diff --git a/__tests__/integration/server/helpers.ts b/__tests__/integration/server/helpers.ts new file mode 100644 index 0000000..4a6fd3c --- /dev/null +++ b/__tests__/integration/server/helpers.ts @@ -0,0 +1,99 @@ +/** + * Shared helpers for server integration tests. + * + * The tests in this directory are designed to run against a live HotCRM server + * (started via `scripts/start-integration-server.sh`). + * + * They are NOT part of the regular `jest` run — use `pnpm test:integration:server`. + */ + +/** Base URL for the integration server (default: http://localhost:4000). */ +export const BASE_URL = + process.env.INTEGRATION_SERVER_URL || "http://localhost:4000"; + +/** Convenience wrapper around fetch that prefixes BASE_URL. */ +export async function api( + path: string, + init?: RequestInit, +): Promise { + const url = `${BASE_URL}${path}`; + return fetch(url, { + ...init, + headers: { + "Content-Type": "application/json", + ...init?.headers, + }, + }); +} + +/** Extract JSON body and assert the response was successful. */ +export async function apiOk( + path: string, + init?: RequestInit, +): Promise { + const res = await api(path, init); + if (!res.ok) { + const text = await res.text().catch(() => "(no body)"); + throw new Error( + `API ${init?.method ?? "GET"} ${path} returned ${res.status}: ${text}`, + ); + } + return res.json() as Promise; +} + +/** Generate a unique email for each test run to avoid collisions. */ +export function uniqueEmail(): string { + const ts = Date.now(); + const rand = Math.random().toString(36).slice(2, 8); + return `test-${ts}-${rand}@integration.test`; +} + +/** + * Register + login helper. + * Returns the raw cookie header from the auth response so subsequent + * requests can be authenticated. + */ +export async function registerAndLogin(overrides?: { + email?: string; + password?: string; + name?: string; +}): Promise<{ email: string; cookie: string }> { + const email = overrides?.email ?? uniqueEmail(); + const password = overrides?.password ?? "IntTest_Passw0rd!"; + const name = overrides?.name ?? "Integration Tester"; + + // Register + const regRes = await api("/api/v1/auth/sign-up/email", { + method: "POST", + body: JSON.stringify({ email, password, name }), + }); + + if (!regRes.ok) { + const body = await regRes.text().catch(() => ""); + throw new Error(`Registration failed (${regRes.status}): ${body}`); + } + + // The server sets session cookies on successful registration + const setCookie = regRes.headers.get("set-cookie") ?? ""; + + // If we already have a session cookie from registration, use it + if (setCookie) { + return { email, cookie: setCookie }; + } + + // Otherwise, login explicitly + const loginRes = await api("/api/v1/auth/sign-in/email", { + method: "POST", + body: JSON.stringify({ email, password }), + }); + + if (!loginRes.ok) { + const body = await loginRes.text().catch(() => ""); + throw new Error(`Login failed (${loginRes.status}): ${body}`); + } + + return { + email, + cookie: loginRes.headers.get("set-cookie") ?? "", + }; +} diff --git a/__tests__/integration/server/metadata.integration.test.ts b/__tests__/integration/server/metadata.integration.test.ts new file mode 100644 index 0000000..88e3037 --- /dev/null +++ b/__tests__/integration/server/metadata.integration.test.ts @@ -0,0 +1,114 @@ +/** + * Server Integration Tests — Object Metadata + * + * These tests validate that the HotCRM server correctly exposes + * object metadata: schemas, fields, views, and package information. + * + * Prerequisites: + * 1. HotCRM server running: `./scripts/start-integration-server.sh --bg` + * 2. Server is ready: `./scripts/wait-for-server.sh` + * + * Run: + * pnpm test:integration:server + */ + +import { api, registerAndLogin } from "./helpers"; + +describe("Object Metadata", () => { + let cookie = ""; + + beforeAll(async () => { + const auth = await registerAndLogin(); + cookie = auth.cookie; + }); + + const authed = (init?: RequestInit): RequestInit => ({ + ...init, + headers: { + ...init?.headers, + ...(cookie ? { Cookie: cookie } : {}), + }, + }); + + describe("Packages / Apps", () => { + it("should list available packages", async () => { + const res = await api("/api/v1/packages", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + // HotCRM has: crm, finance, marketing, products, support, hr + const packages = body.packages ?? body.data ?? body; + if (Array.isArray(packages)) { + expect(packages.length).toBeGreaterThan(0); + } + } else { + // Endpoint might not exist in every server configuration + expect([404, 405, 501]).toContain(res.status); + } + }); + }); + + describe("Object Fields", () => { + it("should return fields for the account object", async () => { + const res = await api("/api/v1/objects/account/fields", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + const fields = body.fields ?? body.data ?? body; + if (Array.isArray(fields)) { + expect(fields.length).toBeGreaterThan(0); + } + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + + it("should return fields for the contact object", async () => { + const res = await api("/api/v1/objects/contact/fields", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); + + describe("Object Views", () => { + it("should return views for the account object", async () => { + const res = await api("/api/v1/objects/account/views", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + const views = body.views ?? body.data ?? body; + if (Array.isArray(views)) { + expect(views.length).toBeGreaterThan(0); + } + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); + + describe("Object List", () => { + it("should list available objects", async () => { + const res = await api("/api/v1/objects", authed()); + + if (res.ok) { + const body = await res.json(); + expect(body).toBeDefined(); + const objects = body.objects ?? body.data ?? body; + if (Array.isArray(objects)) { + // HotCRM defines 65 objects across all plugins + expect(objects.length).toBeGreaterThan(0); + } + } else { + expect([404, 405, 501]).toContain(res.status); + } + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index 198a3ab..a9b6490 100644 --- a/jest.config.js +++ b/jest.config.js @@ -22,6 +22,11 @@ module.exports = { "**/__tests__/**/*.(test|spec).(ts|tsx)", "**/*.(test|spec).(ts|tsx)", ], + testPathIgnorePatterns: [ + "/node_modules/", + "\\.integration\\.test\\.(ts|tsx)$", + "/server/hotcrm/", + ], collectCoverageFrom: [ "lib/**/*.{ts,tsx}", "hooks/**/*.{ts,tsx}", diff --git a/jest.integration.config.js b/jest.integration.config.js new file mode 100644 index 0000000..8838619 --- /dev/null +++ b/jest.integration.config.js @@ -0,0 +1,33 @@ +/** + * Jest configuration for server integration tests. + * + * These tests run against a live HotCRM server and must NOT be included + * in the normal unit-test run (which uses MSW mocks). + * + * Usage: + * npx jest --config jest.integration.config.js + */ +module.exports = { + // Minimal preset — no React Native or Expo transforms needed + testEnvironment: "node", + + // Only match the server integration tests + testMatch: [ + "**/__tests__/integration/server/**/*.integration.test.(ts|tsx)", + ], + + // TypeScript transform + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "tsconfig.json", + // Skip type checking for speed — these are runtime tests + diagnostics: false, + }, + ], + }, + + // Longer timeout — real network I/O + testTimeout: 30_000, +}; diff --git a/package.json b/package.json index 9b674b2..c0b6a39 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "bundle:analyze": "npx expo export --dump-sourcemap --platform ios && npx source-map-explorer dist/bundles/ios/*.js", "server:dev": "tsx server/dev.ts", "server:test": "tsx server/test-auth.ts", + "server:hotcrm": "./scripts/start-integration-server.sh", + "server:hotcrm:bg": "./scripts/start-integration-server.sh --bg", + "server:hotcrm:stop": "./scripts/stop-integration-server.sh", + "test:integration:server": "jest --config jest.integration.config.js --passWithNoTests", "changeset": "changeset", "version-packages": "changeset version", "release": "changeset publish" @@ -94,6 +98,7 @@ "msw": "^2.12.9", "prettier": "^3.5.3", "react-test-renderer": "19.1.0", + "ts-jest": "^29.4.6", "tsx": "^4.21.0", "typescript": "~5.9.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 075b925..d643720 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,9 @@ importers: react-test-renderer: specifier: 19.1.0 version: 19.1.0(react@19.1.0) + ts-jest: + specifier: ^29.4.6 + version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -3008,6 +3011,10 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -4247,6 +4254,11 @@ packages: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -5122,6 +5134,9 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -5174,6 +5189,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} @@ -5558,6 +5576,9 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} @@ -6860,6 +6881,33 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.6: + resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6888,6 +6936,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + type-fest@5.4.4: resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} engines: {node: '>=20'} @@ -6917,6 +6969,11 @@ packages: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -7149,6 +7206,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -10717,6 +10777,10 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -12105,6 +12169,15 @@ snapshots: graphql@16.12.0: {} + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + has-bigints@1.1.0: {} has-flag@3.0.0: {} @@ -13163,6 +13236,8 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} lodash.startcase@4.4.0: {} @@ -13207,6 +13282,8 @@ snapshots: dependencies: semver: 7.7.4 + make-error@1.3.6: {} + makeerror@1.0.12: dependencies: tmpl: 1.0.5 @@ -13955,6 +14032,8 @@ snapshots: negotiator@1.0.0: {} + neo-async@2.6.2: {} + nested-error-stacks@2.0.1: {} next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -15481,6 +15560,26 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@25.2.2))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@25.2.2) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.29.0) + jest-util: 29.7.0 + tslib@2.8.1: {} tsx@4.21.0: @@ -15502,6 +15601,8 @@ snapshots: type-fest@0.7.1: {} + type-fest@4.41.0: {} + type-fest@5.4.4: dependencies: tagged-tag: 1.0.0 @@ -15543,6 +15644,9 @@ snapshots: ua-parser-js@1.0.41: {} + uglify-js@3.19.3: + optional: true + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -15806,6 +15910,8 @@ snapshots: word-wrap@1.2.5: {} + wordwrap@1.0.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 diff --git a/scripts/start-integration-server.sh b/scripts/start-integration-server.sh new file mode 100755 index 0000000..3eae61a --- /dev/null +++ b/scripts/start-integration-server.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# Start the HotCRM integration test server. +# +# Usage: +# ./scripts/start-integration-server.sh # foreground (default) +# ./scripts/start-integration-server.sh --bg # background (writes PID to .hotcrm-server.pid) +# +# Environment: +# PORT – server port (default 4000) +# HOTCRM_DIR – path to the hotcrm submodule (default server/hotcrm) +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +HOTCRM_DIR="${HOTCRM_DIR:-$ROOT_DIR/server/hotcrm}" +PORT="${PORT:-4000}" + +if [ ! -d "$HOTCRM_DIR/packages" ]; then + echo "❌ HotCRM submodule not found at $HOTCRM_DIR" + echo " Run: git submodule update --init --recursive" + exit 1 +fi + +echo "📦 Installing HotCRM dependencies…" +cd "$HOTCRM_DIR" +pnpm install --frozen-lockfile 2>/dev/null || pnpm install + +echo "🔨 Building HotCRM packages…" +pnpm build + +export PORT + +if [ "${1:-}" = "--bg" ]; then + echo "🚀 Starting HotCRM server in background on port $PORT…" + nohup pnpm dev > "$ROOT_DIR/.hotcrm-server.log" 2>&1 & + SERVER_PID=$! + echo "$SERVER_PID" > "$ROOT_DIR/.hotcrm-server.pid" + echo " PID: $SERVER_PID" + echo " Log: $ROOT_DIR/.hotcrm-server.log" +else + echo "🚀 Starting HotCRM server on port $PORT…" + exec pnpm dev +fi diff --git a/scripts/stop-integration-server.sh b/scripts/stop-integration-server.sh new file mode 100755 index 0000000..bf5d228 --- /dev/null +++ b/scripts/stop-integration-server.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# +# Stop the background HotCRM integration test server. +# +# Reads the PID from .hotcrm-server.pid written by start-integration-server.sh --bg +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +PID_FILE="$ROOT_DIR/.hotcrm-server.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "ℹ️ No PID file found – server may not be running." + exit 0 +fi + +PID="$(cat "$PID_FILE")" + +if kill -0 "$PID" 2>/dev/null; then + echo "🛑 Stopping HotCRM server (PID $PID)…" + kill "$PID" + # Wait briefly for graceful shutdown + for _ in $(seq 1 10); do + kill -0 "$PID" 2>/dev/null || break + sleep 1 + done + # Force kill if still running + if kill -0 "$PID" 2>/dev/null; then + kill -9 "$PID" 2>/dev/null || true + fi + echo "✅ Server stopped." +else + echo "ℹ️ Process $PID is not running." +fi + +rm -f "$PID_FILE" diff --git a/scripts/wait-for-server.sh b/scripts/wait-for-server.sh new file mode 100755 index 0000000..b1f0613 --- /dev/null +++ b/scripts/wait-for-server.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Wait until a server is accepting HTTP connections. +# +# Usage: +# ./scripts/wait-for-server.sh [URL] [TIMEOUT_SECONDS] +# +# Defaults: +# URL = http://localhost:4000/api/v1/auth/get-session +# TIMEOUT = 60 + +set -euo pipefail + +URL="${1:-http://localhost:4000/api/v1/auth/get-session}" +TIMEOUT="${2:-60}" + +echo "⏳ Waiting for server at $URL (timeout: ${TIMEOUT}s)…" + +ELAPSED=0 +while [ "$ELAPSED" -lt "$TIMEOUT" ]; do + if curl -sf -o /dev/null "$URL" 2>/dev/null; then + echo "✅ Server is ready (${ELAPSED}s)" + exit 0 + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) +done + +echo "❌ Server did not become ready within ${TIMEOUT}s" +exit 1 diff --git a/server/hotcrm b/server/hotcrm new file mode 160000 index 0000000..19305c0 --- /dev/null +++ b/server/hotcrm @@ -0,0 +1 @@ +Subproject commit 19305c01aa52b2bcbf5fce2edb52869c135b4526 From a2a672422790c350c694b4622870f21d0197eeea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:51:53 +0000 Subject: [PATCH 3/3] fix: exclude hotcrm submodule from TS/ESLint checks, remove unused import Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- .eslintrc.js | 2 +- __tests__/integration/server/crud.integration.test.ts | 2 +- tsconfig.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index d807622..f9ee2ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,5 +36,5 @@ module.exports = { }, }, ], - ignorePatterns: ["node_modules/", ".expo/", "dist/", "*.config.js", "babel.config.js"], + ignorePatterns: ["node_modules/", ".expo/", "dist/", "*.config.js", "babel.config.js", "server/hotcrm/"], }; diff --git a/__tests__/integration/server/crud.integration.test.ts b/__tests__/integration/server/crud.integration.test.ts index fa04cd9..6306a85 100644 --- a/__tests__/integration/server/crud.integration.test.ts +++ b/__tests__/integration/server/crud.integration.test.ts @@ -12,7 +12,7 @@ * pnpm test:integration:server */ -import { api, apiOk, registerAndLogin } from "./helpers"; +import { api, registerAndLogin } from "./helpers"; describe("CRUD Operations", () => { let cookie = ""; diff --git a/tsconfig.json b/tsconfig.json index 7d87d94..5a10a9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ ], "exclude": [ "node_modules", - "./apps" + "./apps", + "./server/hotcrm" ] }