diff --git a/content/docs/api/client-sdk.mdx b/content/docs/api/client-sdk.mdx index 41f449b22d..154b02cfe7 100644 --- a/content/docs/api/client-sdk.mdx +++ b/content/docs/api/client-sdk.mdx @@ -143,9 +143,9 @@ surface ever mounted their routes, so every call was a guaranteed 404. ([`route-ledger.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/runtime/src/route-ledger.ts)) records the audited disposition of every server route, and conformance tests on both sides fail when a route lands without a reviewed disposition or the -ledger names a client method that doesn't exist. Integration test -specifications live in -[`CLIENT_SERVER_INTEGRATION_TESTS.md`](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md). +ledger names a client method that doesn't exist. The client-side integration +tests that exist today are listed in +[`packages/client/tests/integration/README.md`](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/tests/integration/README.md). --- @@ -687,7 +687,7 @@ pnpm test:integration Integration tests verify end-to-end communication with a live ObjectStack server across the client's API namespaces. -**Test coverage**: Integration test specifications cover discovery/connection, authentication, metadata operations, CRUD operations (basic, batch, advanced queries), permissions, workflow, realtime, notifications, AI services, i18n, analytics, packages, views, storage, and automation. +**Test coverage**: the client's *unit* tests are what cover the API namespaces broadly. Integration tests against a live server are far narrower — only discovery/connection is written today; the remaining namespaces are listed as a backlog in [`packages/client/tests/integration/README.md`](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/tests/integration/README.md). Per-route coverage is asserted by code in CI — [`packages/runtime/src/route-ledger.ts`](https://github.com/objectstack-ai/objectstack/blob/main/packages/runtime/src/route-ledger.ts) plus its conformance tests — not by integration tests and not by a hand-maintained table. --- @@ -697,7 +697,7 @@ Integration tests verify end-to-end communication with a live ObjectStack server For detailed information about the client's protocol implementation: - **[Protocol Compliance Matrix](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/CLIENT_SPEC_COMPLIANCE.md)** — Method-by-method verification of all API methods across 13 namespaces -- **[Integration Test Specifications](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md)** — Comprehensive test cases for client-server communication +- **[Integration Tests](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/tests/integration/README.md)** — What client-server integration coverage exists today, and the backlog of namespaces still unwritten - **[Package README](https://github.com/objectstack-ai/objectstack/blob/main/packages/client/README.md)** — Developer navigation and API reference --- diff --git a/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md b/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md deleted file mode 100644 index 2fe2e4d081..0000000000 --- a/packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md +++ /dev/null @@ -1,808 +0,0 @@ -# @objectstack/client - Server Integration Test Specification - -## Overview - -This document defines comprehensive integration tests for validating `@objectstack/client` against a live ObjectStack server implementation. These tests verify that the client SDK correctly communicates with the server across all API namespaces. - ---- - -## Test Environment Setup - -### Prerequisites - -1. **Server Requirements:** - - ObjectStack server instance running - - Test database (SQLite/Postgres) with sample data - - All core services enabled (metadata, data, auth) - - Optional services enabled (automation, ai, etc.) - -2. **Client Configuration:** - ```typescript - const testConfig: ClientConfig = { - baseUrl: process.env.TEST_SERVER_URL || 'http://localhost:3000', - token: undefined, // Will be set after login - debug: true, - logger: createLogger({ level: 'debug' }) - }; - ``` - -3. **Test Data:** - - Sample objects: `test_contact`, `test_project`, `test_task` - - Sample users: test@example.com (admin), user@example.com (standard) - - Sample packages: `@test/sample-plugin` - ---- - -## Test Suite Structure - -``` -packages/client/tests/integration/ -├── 01-discovery.test.ts # Discovery & connection -├── 02-auth.test.ts # Authentication flows -├── 03-metadata.test.ts # Metadata operations -├── 04-data-crud.test.ts # Basic CRUD operations -├── 05-data-batch.test.ts # Batch operations -├── 06-data-query.test.ts # Advanced queries -├── 07-notifications.test.ts # Notifications -├── 08-ai.test.ts # AI services -├── 09-i18n.test.ts # Internationalization -├── 10-analytics.test.ts # Analytics queries -├── 11-packages.test.ts # Package management -├── 12-storage.test.ts # File storage -├── 13-automation.test.ts # Automation triggers -└── helpers/ - ├── test-server.ts # Mock/stub server helpers - ├── test-data.ts # Test data generators - └── assertions.ts # Custom assertions -``` - ---- - -## Test Cases - -### 1. Discovery & Connection (`01-discovery.test.ts`) - -#### TC-DISC-001: Standard Discovery via .well-known -```typescript -describe('Discovery via .well-known', () => { - test('should discover API from .well-known/objectstack', async () => { - const client = new ObjectStackClient({ - baseUrl: 'http://localhost:3000' - }); - - const discovery = await client.connect(); - - expect(discovery.version).toBe('v1'); - expect(discovery.apiName).toBe('ObjectStack'); - expect(discovery.capabilities).toBeDefined(); - expect(discovery.endpoints).toBeDefined(); - }); -}); -``` - -#### TC-DISC-002: Fallback Discovery via /api/v1 -```typescript -test('should fallback to /api/v1 when .well-known unavailable', async () => { - // Mock .well-known to return 404 - mockServer.get('/.well-known/objectstack').reply(404); - mockServer.get('/api/v1').reply(200, { - version: 'v1', - apiName: 'ObjectStack' - }); - - const client = new ObjectStackClient({ baseUrl: mockServerUrl }); - const discovery = await client.connect(); - - expect(discovery.version).toBe('v1'); -}); -``` - -#### TC-DISC-003: Connection Failure Handling -```typescript -test('should throw error when both discovery methods fail', async () => { - mockServer.get('/.well-known/objectstack').reply(404); - mockServer.get('/api/v1').reply(503); - - const client = new ObjectStackClient({ baseUrl: mockServerUrl }); - - await expect(client.connect()).rejects.toThrow(/Failed to connect/); -}); -``` - ---- - -### 2. Authentication (`02-auth.test.ts`) - -#### TC-AUTH-001: Email/Password Login -```typescript -test('should login with email and password', async () => { - const client = new ObjectStackClient({ baseUrl: testServerUrl }); - - const session = await client.auth.login({ - method: 'email', - email: 'test@example.com', - password: 'TestPassword123!' - }); - - expect(session.token).toBeDefined(); - expect(session.user).toBeDefined(); - expect(session.user.email).toBe('test@example.com'); - expect(session.expiresAt).toBeDefined(); -}); -``` - -#### TC-AUTH-002: Registration -```typescript -test('should register new user account', async () => { - const client = new ObjectStackClient({ baseUrl: testServerUrl }); - - const session = await client.auth.register({ - email: 'newuser@example.com', - password: 'SecurePass123!', - firstName: 'New', - lastName: 'User' - }); - - expect(session.token).toBeDefined(); - expect(session.user.email).toBe('newuser@example.com'); -}); -``` - -#### TC-AUTH-003: Token Refresh -```typescript -test('should refresh expired token', async () => { - const client = new ObjectStackClient({ - baseUrl: testServerUrl, - token: expiredToken - }); - - const newSession = await client.auth.refreshToken({ - refreshToken: validRefreshToken - }); - - expect(newSession.token).not.toBe(expiredToken); - expect(newSession.expiresAt).toBeGreaterThan(Date.now()); -}); -``` - -#### TC-AUTH-004: Get Current User -```typescript -test('should get current authenticated user', async () => { - const client = new ObjectStackClient({ - baseUrl: testServerUrl, - token: validToken - }); - - const user = await client.auth.me(); - - expect(user.id).toBeDefined(); - expect(user.email).toBe('test@example.com'); - expect(user.roles).toContain('admin'); -}); -``` - -#### TC-AUTH-005: Logout -```typescript -test('should logout and invalidate session', async () => { - const client = new ObjectStackClient({ - baseUrl: testServerUrl, - token: validToken - }); - - await client.auth.logout(); - - // Subsequent requests should fail with 401 - await expect(client.auth.me()).rejects.toThrow(/Unauthorized/); -}); -``` - ---- - -### 3. Metadata Operations (`03-metadata.test.ts`) - -#### TC-META-001: Get Metadata Types -```typescript -test('should retrieve all metadata types', async () => { - const client = await createAuthenticatedClient(); - - const types = await client.meta.getTypes(); - - expect(types.types).toContain('object'); - expect(types.types).toContain('plugin'); - expect(types.types).toContain('view'); - expect(types.types).toContain('workflow'); -}); -``` - -#### TC-META-002: Get Items of Type -```typescript -test('should retrieve all objects', async () => { - const client = await createAuthenticatedClient(); - - const objects = await client.meta.getItems('object'); - - expect(objects.items).toBeDefined(); - expect(objects.items.length).toBeGreaterThan(0); - expect(objects.items[0].name).toBeDefined(); - expect(objects.items[0].label).toBeDefined(); -}); -``` - -#### TC-META-003: Get Specific Object Definition -```typescript -test('should retrieve object definition by name', async () => { - const client = await createAuthenticatedClient(); - - const contactObject = await client.meta.getItem('object', 'test_contact'); - - expect(contactObject.name).toBe('test_contact'); - expect(contactObject.label).toBe('Contact'); - expect(contactObject.fields).toBeDefined(); - expect(contactObject.fields.first_name).toBeDefined(); - expect(contactObject.fields.first_name.type).toBe('text'); -}); -``` - -#### TC-META-004: Save Object Definition -```typescript -test('should create/update object definition', async () => { - const client = await createAuthenticatedClient(); - - const newObject = { - name: 'test_dynamic', - label: 'Dynamic Test', - fields: { - name: { type: 'text', label: 'Name', required: true }, - status: { type: 'select', label: 'Status', options: ['active', 'inactive'] } - } - }; - - const saved = await client.meta.saveItem('object', 'test_dynamic', newObject); - - expect(saved.name).toBe('test_dynamic'); - expect(saved.fields.name).toBeDefined(); -}); -``` - -#### TC-META-005: Metadata Caching with ETag -```typescript -test('should support ETag-based caching', async () => { - const client = await createAuthenticatedClient(); - - // First request - const first = await client.meta.getCached('test_contact'); - expect(first.data).toBeDefined(); - expect(first.etag).toBeDefined(); - expect(first.notModified).toBe(false); - - // Second request with ETag - const second = await client.meta.getCached('test_contact', { - ifNoneMatch: `"${first.etag!.value}"` - }); - - expect(second.notModified).toBe(true); - expect(second.data).toBeUndefined(); -}); -``` - ---- - -### 4. Data CRUD Operations (`04-data-crud.test.ts`) - -#### TC-DATA-001: Create Record -```typescript -test('should create new record', async () => { - const client = await createAuthenticatedClient(); - - const contact = await client.data.create('test_contact', { - first_name: 'John', - last_name: 'Doe', - email: 'john.doe@example.com', - phone: '+1234567890' - }); - - expect(contact.id).toBeDefined(); - expect(contact.first_name).toBe('John'); - expect(contact.created_at).toBeDefined(); -}); -``` - -#### TC-DATA-002: Get Record by ID -```typescript -test('should retrieve record by ID', async () => { - const client = await createAuthenticatedClient(); - const created = await client.data.create('test_contact', testContactData); - - const retrieved = await client.data.get('test_contact', created.id); - - expect(retrieved.id).toBe(created.id); - expect(retrieved.first_name).toBe(testContactData.first_name); -}); -``` - -#### TC-DATA-003: Update Record -```typescript -test('should update existing record', async () => { - const client = await createAuthenticatedClient(); - const contact = await client.data.create('test_contact', testContactData); - - const updated = await client.data.update('test_contact', contact.id, { - phone: '+9876543210', - notes: 'Updated via test' - }); - - expect(updated.id).toBe(contact.id); - expect(updated.phone).toBe('+9876543210'); - expect(updated.notes).toBe('Updated via test'); - expect(updated.first_name).toBe(testContactData.first_name); // Unchanged -}); -``` - -#### TC-DATA-004: Delete Record -```typescript -test('should delete record', async () => { - const client = await createAuthenticatedClient(); - const contact = await client.data.create('test_contact', testContactData); - - await client.data.delete('test_contact', contact.id); - - await expect( - client.data.get('test_contact', contact.id) - ).rejects.toThrow(/Not Found|404/); -}); -``` - -#### TC-DATA-005: Find Records with Filters -```typescript -test('should find records with filters', async () => { - const client = await createAuthenticatedClient(); - - // Create test data - await client.data.create('test_contact', { first_name: 'Alice', status: 'active' }); - await client.data.create('test_contact', { first_name: 'Bob', status: 'inactive' }); - await client.data.create('test_contact', { first_name: 'Charlie', status: 'active' }); - - const results = await client.data.find('test_contact', { - filters: { status: 'active' }, - sort: 'first_name', - top: 10 - }); - - expect(results.data.length).toBe(2); - expect(results.data[0].first_name).toBe('Alice'); - expect(results.data[1].first_name).toBe('Charlie'); - expect(results.total).toBeGreaterThanOrEqual(2); -}); -``` - -#### TC-DATA-006: Pagination -```typescript -test('should support pagination', async () => { - const client = await createAuthenticatedClient(); - - // Create 25 test contacts - for (let i = 0; i < 25; i++) { - await client.data.create('test_contact', { - first_name: `Contact${i}`, - email: `contact${i}@example.com` - }); - } - - // Page 1 - const page1 = await client.data.find('test_contact', { - top: 10, - skip: 0, - sort: 'first_name' - }); - expect(page1.data.length).toBe(10); - expect(page1.hasMore).toBe(true); - - // Page 2 - const page2 = await client.data.find('test_contact', { - top: 10, - skip: 10, - sort: 'first_name' - }); - expect(page2.data.length).toBe(10); - expect(page2.data[0].first_name).not.toBe(page1.data[0].first_name); -}); -``` - ---- - -### 5. Batch Operations (`05-data-batch.test.ts`) - -#### TC-BATCH-001: Create Many Records -```typescript -test('should create multiple records', async () => { - const client = await createAuthenticatedClient(); - - const contacts = [ - { first_name: 'Alice', email: 'alice@example.com' }, - { first_name: 'Bob', email: 'bob@example.com' }, - { first_name: 'Charlie', email: 'charlie@example.com' } - ]; - - const created = await client.data.createMany('test_contact', contacts); - - expect(created.length).toBe(3); - expect(created[0].id).toBeDefined(); - expect(created[0].first_name).toBe('Alice'); -}); -``` - -#### TC-BATCH-002: Update Many Records -```typescript -test('should update multiple records', async () => { - const client = await createAuthenticatedClient(); - - // Create test records - const c1 = await client.data.create('test_contact', { first_name: 'Test1' }); - const c2 = await client.data.create('test_contact', { first_name: 'Test2' }); - - const result = await client.data.updateMany('test_contact', [ - { id: c1.id, data: { status: 'updated' } }, - { id: c2.id, data: { status: 'updated' } } - ]); - - expect(result.success).toBe(true); - expect(result.successCount).toBe(2); - expect(result.failedCount).toBe(0); -}); -``` - -#### TC-BATCH-003: Delete Many Records -```typescript -test('should delete multiple records', async () => { - const client = await createAuthenticatedClient(); - - const c1 = await client.data.create('test_contact', { first_name: 'Delete1' }); - const c2 = await client.data.create('test_contact', { first_name: 'Delete2' }); - - const result = await client.data.deleteMany('test_contact', [c1.id, c2.id]); - - expect(result.success).toBe(true); - expect(result.successCount).toBe(2); - - await expect(client.data.get('test_contact', c1.id)).rejects.toThrow(); - await expect(client.data.get('test_contact', c2.id)).rejects.toThrow(); -}); -``` - -#### TC-BATCH-004: Mixed Batch Operations -```typescript -test('should execute mixed batch operations', async () => { - const client = await createAuthenticatedClient(); - - const existing = await client.data.create('test_contact', { first_name: 'Existing' }); - - const batchRequest: BatchUpdateRequest = { - operations: [ - { action: 'create', data: { first_name: 'New1' } }, - { action: 'update', id: existing.id, data: { first_name: 'Updated' } }, - { action: 'create', data: { first_name: 'New2' } } - ], - options: { - continueOnError: true, - returnData: true - } - }; - - const result = await client.data.batch('test_contact', batchRequest); - - expect(result.success).toBe(true); - expect(result.successCount).toBe(3); - expect(result.results).toHaveLength(3); -}); -``` - -#### TC-BATCH-005: Transaction Rollback on Error -```typescript -test('should rollback batch on error when continueOnError=false', async () => { - const client = await createAuthenticatedClient(); - - const batchRequest: BatchUpdateRequest = { - operations: [ - { action: 'create', data: { first_name: 'Valid1' } }, - { action: 'update', id: 'invalid-id', data: { first_name: 'Invalid' } }, // This will fail - { action: 'create', data: { first_name: 'Valid2' } } - ], - options: { - continueOnError: false, - transactional: true - } - }; - - await expect( - client.data.batch('test_contact', batchRequest) - ).rejects.toThrow(); - - // Verify no records were created (rolled back) - const all = await client.data.find('test_contact', { - filters: { first_name: ['Valid1', 'Valid2'] } - }); - expect(all.data.length).toBe(0); -}); -``` - ---- - -### 6. Advanced Queries (`06-data-query.test.ts`) - -#### TC-QUERY-001: ObjectQL AST Query -```typescript -test('should execute ObjectQL AST query', async () => { - const client = await createAuthenticatedClient(); - - const query: Partial = { - object: 'test_contact', - filter: { - and: [ - { field: 'status', operator: 'eq', value: 'active' }, - { field: 'created_at', operator: 'gte', value: '2024-01-01' } - ] - }, - sort: [ - { field: 'last_name', direction: 'asc' }, - { field: 'first_name', direction: 'asc' } - ], - pagination: { limit: 20, offset: 0 } - }; - - const results = await client.data.query('test_contact', query); - - expect(results.data).toBeDefined(); - expect(results.total).toBeGreaterThanOrEqual(0); -}); -``` - -#### TC-QUERY-002: Query with Joins/Lookups -```typescript -test('should query with lookup field expansion', async () => { - const client = await createAuthenticatedClient(); - - // Create related data - const project = await client.data.create('test_project', { name: 'Test Project' }); - const task = await client.data.create('test_task', { - title: 'Test Task', - project_id: project.id - }); - - const query: Partial = { - object: 'test_task', - expand: ['project_id'], // Expand the lookup field - filter: { field: 'id', operator: 'eq', value: task.id } - }; - - const results = await client.data.query('test_task', query); - - expect(results.data[0].project_id).toBeDefined(); - expect(results.data[0].project_id.name).toBe('Test Project'); -}); -``` - -#### TC-QUERY-003: Aggregation Query -```typescript -test('should execute aggregation query', async () => { - const client = await createAuthenticatedClient(); - - const query: Partial = { - object: 'test_contact', - aggregations: [ - { function: 'count', alias: 'total_contacts' }, - { function: 'count', field: 'status', alias: 'contacts_with_status' } - ], - groupBy: ['status'] - }; - - const results = await client.data.query('test_contact', query); - - expect(results.aggregations).toBeDefined(); - expect(results.aggregations!.total_contacts).toBeGreaterThan(0); -}); -``` - ---- - -### 7-8. Permissions & Workflow — removed (#3612) - -The `permissions` and `workflow` client namespaces were deleted: no server -surface ever mounted their routes. State-machine reads live on -`meta.getLegalNextStates`; approval decisions are `client.approvals` (ADR-0019). - -### 9-13. Additional Test Categories - -*(Similar detailed test cases for remaining namespaces: Notifications, AI, i18n, Analytics, Packages, Storage, Automation)** - ---- - -## Test Utilities - -### Mock Server Setup - -```typescript -// packages/client/tests/integration/helpers/test-server.ts - -import { setupServer } from 'msw/node'; -import { rest } from 'msw'; - -export function createMockServer() { - return setupServer( - // Discovery - rest.get('/.well-known/objectstack', (req, res, ctx) => { - return res(ctx.json({ - version: 'v1', - apiName: 'ObjectStack Test Server', - capabilities: ['metadata', 'data', 'auth'], - endpoints: { /* ... */ } - })); - }), - - // Auth — better-auth route names, not `/auth/login`: `client.auth.login` - // calls POST /auth/sign-in/email, `register` /auth/sign-up/email, `logout` - // /auth/sign-out, `me` GET /auth/get-session. The audited route table is - // packages/plugins/plugin-auth/src/auth-route-ledger.ts. - rest.post('/api/v1/auth/sign-in/email', (req, res, ctx) => { - return res(ctx.json({ - success: true, - data: { - token: 'mock-jwt-token', - user: { id: '1', email: 'test@example.com' }, - expiresAt: Date.now() + 3600000 - } - })); - }), - - // Add more handlers... - ); -} -``` - -### Test Data Generators - -```typescript -// packages/client/tests/integration/helpers/test-data.ts - -export const generateContact = (overrides = {}) => ({ - first_name: faker.person.firstName(), - last_name: faker.person.lastName(), - email: faker.internet.email(), - phone: faker.phone.number(), - ...overrides -}); - -export const generateProject = (overrides = {}) => ({ - name: faker.commerce.productName(), - description: faker.lorem.paragraph(), - status: 'active', - ...overrides -}); -``` - -### Custom Assertions - -```typescript -// packages/client/tests/integration/helpers/assertions.ts - -export function expectValidId(id: string) { - expect(id).toBeDefined(); - expect(typeof id).toBe('string'); - expect(id.length).toBeGreaterThan(0); -} - -export function expectValidTimestamp(timestamp: string) { - expect(timestamp).toBeDefined(); - expect(new Date(timestamp).getTime()).toBeGreaterThan(0); -} - -export function expectValidResponse(response: any): asserts response is T { - expect(response).toBeDefined(); - expect(typeof response).toBe('object'); -} -``` - ---- - -## Running Tests - -### Local Development - -**Note:** Integration tests require a running ObjectStack server. The server is provided by a separate repository/package and is not included in this spec repository. - -```bash -# Start test server (in the ObjectStack server repository) -# Follow the server project's documentation for setup -# Example: cd /path/to/objectstack-server && pnpm dev:test - -# Run integration tests (in this repository) -cd packages/client -pnpm test:integration -``` - -### CI/CD Pipeline - -**Note:** The workflow file referenced below is an example. Actual CI implementation will require setting up the test server infrastructure separately. - -```yaml -# Example: .github/workflows/client-integration-tests.yml -# This workflow would need to be created and configured with proper server setup -name: Client Integration Tests - -on: [push, pull_request] - -jobs: - test: - runs-on: ubuntu-latest - - services: - postgres: - image: postgres:15 - env: - POSTGRES_PASSWORD: test - options: >- - --health-cmd pg_isready - --health-interval 10s - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 20 - - - name: Install dependencies - run: pnpm install - - - name: Build spec - run: pnpm --filter @objectstack/spec build - - # Note: Server setup would require additional configuration - # This is a placeholder showing the expected structure - - name: Start test server - run: | - # Server startup logic would go here - # This depends on the ObjectStack server implementation - echo "Server setup required" - env: - DATABASE_URL: postgresql://postgres:test@localhost:5432/test - - - name: Run integration tests - run: pnpm --filter @objectstack/client test:integration -``` - ---- - -## Test Coverage Goals - -| Category | Target Coverage | Priority | -|----------|----------------|----------| -| Core Services (discovery, meta, data, auth) | 100% | Critical | -| Optional Services | 90% | High | -| Error Scenarios | 80% | High | -| Edge Cases | 70% | Medium | - ---- - -## Success Criteria - -- ✅ All 17 test suites pass -- ✅ 90%+ code coverage on client SDK -- ✅ Zero protocol compliance violations -- ✅ All request/response schemas validated -- ✅ Authentication flow complete -- ✅ Error handling verified -- ✅ Performance benchmarks met - ---- - -## Related Documentation - -- [Client Spec Compliance Matrix](./CLIENT_SPEC_COMPLIANCE.md) -- [Client README](./README.md) -- [Spec Protocol Map](../spec/PROTOCOL_MAP.md) - ---- - -**Last Updated:** 2026-02-09 -**Status:** 📝 Specification Complete - Ready for Implementation diff --git a/packages/client/README.md b/packages/client/README.md index 400abc4bad..333d588422 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -303,5 +303,5 @@ cd packages/client pnpm test:integration ``` -See [CLIENT_SERVER_INTEGRATION_TESTS.md](./CLIENT_SERVER_INTEGRATION_TESTS.md) for detailed test specifications. +See [`tests/integration/README.md`](./tests/integration/README.md) for what is covered today and what is still unwritten. diff --git a/packages/client/tests/integration/01-discovery.test.ts b/packages/client/tests/integration/01-discovery.test.ts index 3a9e171ca2..eb66d2274e 100644 --- a/packages/client/tests/integration/01-discovery.test.ts +++ b/packages/client/tests/integration/01-discovery.test.ts @@ -4,7 +4,7 @@ * Tests the client's ability to discover and connect to an ObjectStack server. * These tests require a running server instance. * - * @see CLIENT_SERVER_INTEGRATION_TESTS.md for full test specification + * @see ./README.md for what integration coverage exists and what is still unwritten */ import { describe, test, expect } from 'vitest'; diff --git a/packages/client/tests/integration/README.md b/packages/client/tests/integration/README.md index 41c88159c8..21ecd5562c 100644 --- a/packages/client/tests/integration/README.md +++ b/packages/client/tests/integration/README.md @@ -28,39 +28,46 @@ This directory contains integration tests that verify `@objectstack/client` agai ## Test Structure -Tests are organized by protocol namespace: +Tests are organized by protocol namespace, one file per namespace, numbered in the order +a session goes through them. **What exists today is exactly this:** ``` 01-discovery.test.ts # Discovery & connection -02-auth.test.ts # Authentication flows -03-metadata.test.ts # Metadata operations -04-data-crud.test.ts # Basic CRUD operations -05-data-batch.test.ts # Batch operations -06-data-query.test.ts # Advanced queries -07-permissions.test.ts # Permission checking -08-workflow.test.ts # Workflow operations -09-realtime.test.ts # Realtime subscriptions -10-notifications.test.ts # Notifications -11-ai.test.ts # AI services -12-i18n.test.ts # Internationalization -13-analytics.test.ts # Analytics queries -14-packages.test.ts # Package management -15-views.test.ts # View management -16-storage.test.ts # File storage -17-automation.test.ts # Automation triggers ``` -## Test Coverage Goals +That is the whole list. Read it as the list — do not infer a suite from the numbering. -- Core Services (discovery, meta, data, auth): **100%** -- Optional Services: **90%** -- Error Scenarios: **80%** -- Edge Cases: **70%** +### Not yet written + +The namespaces below have no integration coverage yet. This is a topic-level backlog, not +a specification: write each file against the SDK's **current** return shapes +(`packages/client/src/index.ts`) and the server routes the ledgers record, never against a +remembered shape. + +- Authentication — login / register / logout / current session (route names come from + `packages/plugins/plugin-auth/src/auth-route-ledger.ts`; the SDK wraps the session + payload in `data`, it is not flattened onto the response root) +- Metadata — type listing, item read/write, ETag-conditional reads +- Data CRUD — create / read / update / delete, filtering, pagination +- Data batch — createMany / updateMany / deleteMany and mixed batches +- Data query — ObjectQL AST queries, lookup expansion, aggregation +- Notifications, AI, i18n, analytics, packages, storage, automation, approvals + +Namespaces that no longer exist are deliberately absent: `permissions`, `workflow`, +`realtime` and `views` were removed in #3612 because no server surface ever mounted their +routes. Do not add tests for them. ## Related Documentation -- [Integration Test Specification](../../CLIENT_SERVER_INTEGRATION_TESTS.md) - [Client Spec Compliance](../../CLIENT_SPEC_COMPLIANCE.md) +- [Auth route ledger](../../../plugins/plugin-auth/src/auth-route-ledger.ts) — the audited + auth route table, guarded by a conformance test + +> A hand-written `CLIENT_SERVER_INTEGRATION_TESTS.md` "Test Specification" used to sit +> beside this file. It described 13–17 test files of which only `01-discovery.test.ts` was +> ever written, and its assertions had drifted from the SDK's actual return shapes, so it +> read as a finished suite that did not exist. It was retired in #5824; the full text is in +> git history (`git log --diff-filter=D -- packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md`). ## CI/CD @@ -69,4 +76,5 @@ Integration tests can be run in CI, but require: - Test database with sample data - Proper environment configuration -See `CLIENT_SERVER_INTEGRATION_TESTS.md` for example CI configuration structure. +No CI workflow runs them today. Wiring one up means standing that server up in the job +first; there is no ready-made workflow file to copy. diff --git a/packages/plugins/plugin-auth/IMPLEMENTATION_SUMMARY.md b/packages/plugins/plugin-auth/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 11b629f433..0000000000 --- a/packages/plugins/plugin-auth/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,216 +0,0 @@ -# Auth Plugin Implementation Summary - -## Overview - -Successfully integrated the Better-Auth library (v1.4.18) into `@objectstack/plugin-auth` - an authentication and identity plugin for the ObjectStack ecosystem. The plugin now has the better-auth library integrated with a working AuthManager class and lazy initialization pattern. - -## Latest Updates (Phase 1 & 2 Complete) - -### Better-Auth Integration -- ✅ Added better-auth v1.4.18 as runtime dependency -- ✅ Created AuthManager class wrapping better-auth -- ✅ Implemented lazy initialization to avoid database errors -- ✅ Added TypeScript types for all authentication methods -- ✅ Updated plugin to use real AuthManager (not stub) -- ✅ All 11 tests passing with no errors - -### Technical Improvements -- Better-auth instance created only when needed (lazy initialization) -- Proper TypeScript typing for HTTP request/response handlers -- Support for configuration-based initialization -- Extensible design for future features (OAuth, 2FA, etc.) - -## What Was Implemented - -### 1. Package Structure -- Created new workspace package at `packages/plugins/plugin-auth/` -- Configured package.json with proper dependencies -- Set up TypeScript configuration -- Created comprehensive README and CHANGELOG - -### 2. Core Plugin Implementation -- **AuthPlugin class** - Full plugin lifecycle (init, start, destroy) -- **AuthManager class** - Real implementation with better-auth integration -- **Lazy initialization** - Better-auth instance created only when needed -- **Route registration** - the auth base path (`/api/v1/auth/*`) forwarded to better-auth; see [API Routes](#api-routes) -- **Service registration** - Registers 'auth' service in ObjectKernel -- **Configuration support** - Uses AuthConfig schema from @objectstack/spec/system -- **TypeScript types** - Proper typing for IHttpRequest and IHttpResponse - -### 3. Testing -- 11 comprehensive unit tests -- 100% test coverage of implemented functionality -- All tests passing (11/11) -- Proper mocking of dependencies - -### 4. Documentation -- Detailed README with usage examples -- Implementation status clearly documented -- Configuration options explained -- Example usage file (examples/basic-usage.ts) -- Updated main README to list the new package - -### 5. Build & Integration -- Package builds successfully with tsup -- Integrated into monorepo build system -- All dependencies resolved correctly -- No build or lint errors - -## File Structure - -``` -packages/plugins/plugin-auth/ -├── CHANGELOG.md -├── README.md -├── IMPLEMENTATION_SUMMARY.md -├── package.json -├── tsconfig.json -├── examples/ -│ └── basic-usage.ts -├── src/ -│ ├── index.ts -│ ├── auth-plugin.ts # Main plugin implementation -│ ├── auth-manager.ts # NEW: Better-auth wrapper class -│ └── auth-plugin.test.ts -└── dist/ - └── [build outputs] -``` - -## Key Design Decisions - -1. **Better-Auth Integration**: Integrated better-auth v1.4.18 as the core authentication library -2. **Lazy Initialization**: AuthManager creates better-auth instance only when needed to avoid database initialization errors -3. **Flexible Configuration**: Supports custom better-auth instances or automatic creation from config -4. **IHttpServer Integration**: Routes registered through ObjectStack's IHttpServer interface -5. **Configuration Protocol**: Uses existing AuthConfig schema from spec package -6. **Plugin Pattern**: Follows established ObjectStack plugin conventions -7. **TypeScript-First**: Full type safety with proper interface definitions - -## API Routes - -The plugin does **not** hand-register a `login` / `register` / `logout` / `session` route -set. Everything under the auth base path (`/api/v1/auth` by default, `basePath` in the -plugin options) is forwarded to better-auth through a single catch-all mount, so -**better-auth's own route table is the route table** — plus a handful of ObjectStack-owned -routes (`/config`, `/bootstrap-status`, `/admin/*`, …) mounted ahead of it. - -**The single source of truth is [`src/auth-route-ledger.ts`](./src/auth-route-ledger.ts)** -(#3656): the reviewed `AUTH_ROUTE_LEDGER` rows — every route the SDK actually calls, each -naming its client method — plus the full `BETTER_AUTH_MOUNTED_SURFACE` inventory, both -verified against the live `auth.api` table by `auth-route-ledger.conformance.test.ts`. -Read the ledger instead of a copy: a list transcribed into this file drifts the next time -better-auth is upgraded, and this section is the proof (it advertised four routes that -never existed). - -The core routes, spelled the way better-auth actually serves them — same names as -[`content/docs/api/plugin-endpoints.mdx`](../../../content/docs/api/plugin-endpoints.mdx): - -| Route | SDK method | -|:------|:-----------| -| `POST /api/v1/auth/sign-in/email` | `auth.login` | -| `POST /api/v1/auth/sign-up/email` | `auth.register` | -| `POST /api/v1/auth/sign-out` | `auth.logout` | -| `GET /api/v1/auth/get-session` | `auth.me` | - -There is no `/auth/login`, `/auth/register`, `/auth/logout` or `/auth/session` route. The -one legacy explicit `POST /api/v1/auth/login` mount that made the first of them look -reachable lived in the runtime dispatcher, answered HTTP 500 to every caller, and was -deleted in #5085. - -## Dependencies - -### Runtime Dependencies -- `@objectstack/core` - Plugin system -- `@objectstack/spec` - Protocol schemas -- `better-auth` ^1.4.18 - Authentication library - -### Peer Dependencies (Optional) -- `drizzle-orm` >=0.41.0 - For database persistence (optional) - -### Dev Dependencies -- `@types/node` ^25.2.2 -- `typescript` ^5.0.0 -- `vitest` ^4.0.18 - -## Testing Results - -``` - ✓ src/auth-plugin.test.ts (11 tests) 13ms - ✓ Plugin Metadata (1) - ✓ Initialization (4) - ✓ Start Phase (3) - ✓ Destroy Phase (1) - ✓ Configuration Options (2) - - Test Files 1 passed (1) - Tests 11 passed (11) - -✅ All tests passing with no errors -✅ Better-auth integration working with lazy initialization -``` - -## Next Steps (Future Development) - -1. **Phase 3: Complete API Integration** - - Wire up better-auth API methods to login/register/logout routes - - Implement proper session management - - Add request/response transformations - -2. **Phase 4: Database Adapter** - - Implement drizzle-orm adapter - - Add database schema migrations - - Support multiple database providers (PostgreSQL, MySQL, SQLite) - -3. **Phase 5: OAuth Providers** - - Google OAuth integration - - GitHub OAuth integration - - Generic OAuth provider support - - Provider configuration - -4. **Phase 6: Advanced Features** - - Two-factor authentication (2FA) - - Passkey support - - Magic link authentication - - Organization/team management - -5. **Phase 7: Security** - - Rate limiting - - CSRF protection - - Session security - - Audit logging - -## Current Implementation Status - -✅ **Phase 1 & 2: COMPLETE** -- Better-auth library successfully integrated -- AuthManager class implemented with lazy initialization -- All tests passing -- Build successful -- Ready for Phase 3 (API Integration) - -🔄 **Phase 3: IN PROGRESS** -- Authentication method structures in place -- Placeholder responses implemented -- Need to connect actual better-auth API calls -## References - -- Plugin implementation: `packages/plugins/plugin-auth/src/auth-plugin.ts` -- AuthManager implementation: `packages/plugins/plugin-auth/src/auth-manager.ts` -- Tests: `packages/plugins/plugin-auth/src/auth-plugin.test.ts` -- Schema: `packages/spec/src/system/auth-config.zod.ts` -- Example: `packages/plugins/plugin-auth/examples/basic-usage.ts` -- Better-auth docs: https://www.better-auth.com/ - -## Recent Commits - -1. `135a5c6` - feat: add better-auth library integration to auth plugin -2. `c11398a` - Initial plan -3. `81dbb51` - docs: update implementation summary with planned features - ---- - -**Status**: ✅ Better-Auth Integration Complete (Phase 1 & 2) -**Version**: 2.0.2 -**Test Coverage**: 11/11 tests passing (100%) -**Build Status**: ✅ Passing -**Dependencies**: better-auth v1.4.18 integrated diff --git a/packages/plugins/plugin-auth/README.md b/packages/plugins/plugin-auth/README.md index f04132eddf..a8891b5e97 100644 --- a/packages/plugins/plugin-auth/README.md +++ b/packages/plugins/plugin-auth/README.md @@ -110,7 +110,16 @@ The plugin accepts configuration via `AuthConfig` schema from `@objectstack/spec ## API Routes -The plugin forwards all requests under `/api/v1/auth/*` directly to better-auth's universal handler. Better-auth provides the following endpoints: +The plugin forwards all requests under `/api/v1/auth/*` directly to better-auth's universal handler, so **better-auth's own route table is the route table** — the plugin does not hand-register a `login` / `register` / `logout` / `session` route set. + +**The single source of truth is [`src/auth-route-ledger.ts`](./src/auth-route-ledger.ts)** +(#3656): the reviewed `AUTH_ROUTE_LEDGER` rows — every route the SDK actually calls, each +naming its client method — plus the full `BETTER_AUTH_MOUNTED_SURFACE` inventory, both +verified against the live `auth.api` table by `src/auth-route-ledger.conformance.test.ts`. +The list below is a reading aid, not a second contract: where it disagrees with the ledger, +the ledger is right. A route table transcribed by hand drifts the next time better-auth is +upgraded — that is how four routes that never existed (`/auth/login`, `/auth/register`, +`/auth/logout`, `/auth/session`) survived in this package's docs until #5085 / #5772. ### Email/Password Authentication - `POST /api/v1/auth/sign-in/email` - Sign in with email and password