diff --git a/.gitignore b/.gitignore index 09ec47836..f5a356653 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ **/.DS_Store .notion -node_modules/.vite/deps **/.auth #.env diff --git a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json b/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json deleted file mode 100644 index 79ea6c146..000000000 --- a/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"3.2.4","results":[[":src/__tests__/hooks/use-toast.test.tsx",{"duration":91.43616600000007,"failed":true}],[":src/__tests__/hooks/use-mobile.test.tsx",{"duration":60.4452500000001,"failed":true}],[":src/__tests__/utils/utils.test.ts",{"duration":22.88345800000002,"failed":true}],[":src/services/__tests__/stateManagement.test.ts",{"duration":77.12820899999997,"failed":true}],[":src/services/__tests__/storage.test.ts",{"duration":50.88487500000008,"failed":true}],[":src/components/__tests__/ConversationSidebar.test.tsx",{"duration":1353.1905420000003,"failed":true}],[":src/__tests__/components/ConversationSidebar.test.tsx",{"duration":151.526292,"failed":true}],[":src/__tests__/components/ChatInterface.test.tsx",{"duration":113.44941599999993,"failed":true}],[":src/__tests__/components/SettingsPanel.test.tsx",{"duration":144.17041700000004,"failed":true}]]} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 50067bcac..402a4338f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,7 @@ "@types/node": "^24.6.2", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/supertest": "^6.0.3", "@vitejs/plugin-react-swc": "^4.1.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -84,6 +85,7 @@ "jsdom": "^27.0.0", "lovable-tagger": "^1.1.10", "postcss": "^8.5.6", + "supertest": "^7.1.4", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.2", @@ -1832,6 +1834,19 @@ "zod": "^3.24.1" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1926,6 +1941,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -4065,6 +4090,13 @@ "@types/deep-eql": "*" } }, + "node_modules/@types/cookiejar": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", + "integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -4149,6 +4181,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/methods": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", + "integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", @@ -4178,6 +4217,30 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/superagent": { + "version": "8.1.9", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.9.tgz", + "integrity": "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookiejar": "^2.1.5", + "@types/methods": "^1.1.4", + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/supertest": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.3.tgz", + "integrity": "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/methods": "^1.1.4", + "@types/superagent": "^8.1.0" + } + }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -4807,6 +4870,13 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -5380,6 +5450,16 @@ "node": ">= 6" } }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5430,6 +5510,13 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -5804,6 +5891,17 @@ "react": ">=16" } }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6448,6 +6546,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -6666,6 +6771,24 @@ "node": ">=12.20.0" } }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -9876,6 +9999,54 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/superagent": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.2.3.tgz", + "integrity": "sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.4", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.2" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/supertest/-/supertest-7.1.4.tgz", + "integrity": "sha512-tjLPs7dVyqgItVFirHYqe2T+MfWc2VOBQ8QFKKbWTA3PU7liZR8zoSpAi/C1k1ilm9RsXIKYf197oap9wXGVYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "methods": "^1.1.2", + "superagent": "^10.2.3" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index fbfd720b2..e3641aba9 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@types/node": "^24.6.2", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/supertest": "^6.0.3", "@vitejs/plugin-react-swc": "^4.1.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", @@ -96,6 +97,7 @@ "jsdom": "^27.0.0", "lovable-tagger": "^1.1.10", "postcss": "^8.5.6", + "supertest": "^7.1.4", "tailwindcss": "^3.4.17", "tailwindcss-animate": "^1.0.7", "ts-node": "^10.9.2", diff --git a/src/server/chatSyncApi.js b/src/server/chatSyncApi.js index 32f3afb1c..75bed7ef1 100644 --- a/src/server/chatSyncApi.js +++ b/src/server/chatSyncApi.js @@ -4,6 +4,8 @@ import http from 'http'; import { getAllChats, addOrUpdateChats, deleteChat, deleteChats } from './chatStore.js'; +const PORT = process.env.CHAT_SYNC_PORT || 4001; + // Utility: parse JSON body function parseBody(req) { return new Promise((resolve, reject) => { @@ -102,8 +104,12 @@ const server = http.createServer(async (req, res) => { } }); -// Start server -const PORT = process.env.CHAT_SYNC_PORT || 4001; -server.listen(PORT, () => { - console.log(`Chat sync API running on http://localhost:${PORT}`); -}); \ No newline at end of file +// Only start the server if this file is run directly +if (import.meta.url === `file://${process.argv[1]}`) { + server.listen(PORT, () => { + console.log(`Chat sync API running on http://localhost:${PORT}`); + }); +} + +// Export for testing +export { server }; \ No newline at end of file diff --git a/src/test/mocks/api.ts b/src/tests/mocks/api.ts similarity index 100% rename from src/test/mocks/api.ts rename to src/tests/mocks/api.ts diff --git a/src/test/mocks/providers.ts b/src/tests/mocks/providers.ts similarity index 100% rename from src/test/mocks/providers.ts rename to src/tests/mocks/providers.ts diff --git a/src/tests/server/chatSync.unit.test.ts b/src/tests/server/chatSync.unit.test.ts new file mode 100644 index 000000000..34f670391 --- /dev/null +++ b/src/tests/server/chatSync.unit.test.ts @@ -0,0 +1,145 @@ +// src/tests/services/chatSync.unit.test.ts +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; + +// Import the server instance directly (not starting it on a port) +// We need to export the server from chatSyncApi.js first +import { server } from '../../server/chatSyncApi.js'; + +// Create supertest agent - NO network ports used +const agent = request(server); + +describe('Chat Sync API - Unit Tests', () => { + let createdChatId: string; + + // Clean up: delete any test chats before each test + beforeEach(async () => { + // Get all chats + const response = await agent.get('/fetchChats'); + const chats = response.body; + + // Delete any existing test chats + const testChatIds = chats + .filter((chat: any) => chat.id?.startsWith('test_')) + .map((chat: any) => chat.id); + + if (testChatIds.length > 0) { + await agent + .post('/deleteChats') + .send({ chatIds: testChatIds }); + } + }); + + it('should create, delete, and verify removal of a chat', async () => { + // --- 1. CREATE (Push a new chat to the server) --- + const newChat = { + id: `test_conv_${Date.now()}`, + title: 'Test Chat', + messages: [], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + const createResponse = await agent + .post('/pushChats') + .send({ chats: [newChat] }); + + expect(createResponse.statusCode).toBe(200); + expect(createResponse.body).toHaveProperty('ok', true); + createdChatId = newChat.id; + + // Verify it was created + const fetchAfterCreate = await agent.get('/fetchChats'); + expect(fetchAfterCreate.statusCode).toBe(200); + const chatExists = fetchAfterCreate.body.some((chat: any) => chat.id === createdChatId); + expect(chatExists).toBe(true); + + // --- 2. DELETE --- + const deleteResponse = await agent + .delete(`/deleteChat/${createdChatId}`); + + expect(deleteResponse.statusCode).toBe(200); + expect(deleteResponse.body).toHaveProperty('ok', true); + expect(deleteResponse.body).toHaveProperty('deleted', true); + + // --- 3. CONFIRM REMOVAL --- + const fetchAfterDelete = await agent.get('/fetchChats'); + expect(fetchAfterDelete.statusCode).toBe(200); + + const isChatStillPresent = fetchAfterDelete.body.some( + (chat: any) => chat.id === createdChatId + ); + expect(isChatStillPresent).toBe(false); + }); + + it('should handle deleting non-existent chat', async () => { + const response = await agent + .delete('/deleteChat/nonexistent_id_12345'); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('ok', true); + expect(response.body).toHaveProperty('deleted', false); + }); + + it('should handle missing chat ID in delete request', async () => { + const response = await agent + .delete('/deleteChat/'); + + expect(response.statusCode).toBe(400); + }); + + it('should delete multiple chats', async () => { + // Create test chats + const testChats = [ + { + id: `test_1_${Date.now()}`, + title: 'Test 1', + messages: [], + createdAt: Date.now(), + updatedAt: Date.now() + }, + { + id: `test_2_${Date.now()}`, + title: 'Test 2', + messages: [], + createdAt: Date.now(), + updatedAt: Date.now() + } + ]; + + await agent + .post('/pushChats') + .send({ chats: testChats }); + + // Delete them + const response = await agent + .post('/deleteChats') + .send({ chatIds: testChats.map(c => c.id) }); + + expect(response.statusCode).toBe(200); + expect(response.body).toHaveProperty('ok', true); + expect(response.body).toHaveProperty('deletedCount', 2); + + // Verify deletion + const fetchResponse = await agent.get('/fetchChats'); + const remainingIds = fetchResponse.body.map((chat: any) => chat.id); + + testChats.forEach(chat => { + expect(remainingIds).not.toContain(chat.id); + }); + }); + + it('should fetch all chats', async () => { + const response = await agent.get('/fetchChats'); + + expect(response.statusCode).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + }); + + it('should handle CORS preflight', async () => { + const response = await agent.options('/fetchChats'); + + expect(response.statusCode).toBe(204); + expect(response.headers['access-control-allow-origin']).toBe('*'); + }); +}); \ No newline at end of file diff --git a/src/test/setup.ts b/src/tests/setup.ts similarity index 100% rename from src/test/setup.ts rename to src/tests/setup.ts diff --git a/src/test/utils.tsx b/src/tests/utils.tsx similarity index 100% rename from src/test/utils.tsx rename to src/tests/utils.tsx diff --git a/vitest.config.ts b/vitest.config.ts index 04565e6ad..1bca0a2f3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,4 @@ +// vitest.config.ts import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react-swc' import path from 'path' @@ -6,19 +7,20 @@ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', - setupFiles: ['./src/test/setup.ts'], + setupFiles: ['./src/tests/setup.ts'], globals: true, css: true, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ + 'src/tests/', + 'src/__tests__/**/*', 'node_modules/', - 'src/test/', '**/*.d.ts', '**/*.config.*', - 'dist/', - 'coverage/' + 'coverage/', + 'dist/' ] } }, @@ -27,4 +29,5 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, -}) \ No newline at end of file +}) +