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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/"],
};
64 changes: 64 additions & 0 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
@@ -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

Comment on lines +38 to +44
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

CI installs/builds HotCRM twice: once in “Setup HotCRM server”, then again inside start-integration-server.sh (which runs pnpm install + pnpm build). Drop the separate setup step and let the start script do it once, or add a flag/env to the start script (e.g., “skip install/build”) and use it in CI to avoid redundant work.

Suggested change
# Install HotCRM submodule dependencies and build
- name: Setup HotCRM server
run: |
cd server/hotcrm
pnpm install
pnpm build

Copilot uses AI. Check for mistakes.
# 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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "server/hotcrm"]
path = server/hotcrm
url = https://github.com/objectstack-ai/hotcrm.git
131 changes: 131 additions & 0 deletions __tests__/integration/server/auth.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading
Loading