@open-mercato/channel-imap 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/AGENTS.md +56 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.js +62 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.js +19 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.js +16 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.js +26 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.js +27 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.js +15 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.js +15 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.js +6 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.js +6 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.js +6 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.js +6 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.js +48 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.js.map +7 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.js +6 -0
- package/dist/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.js.map +7 -0
- package/dist/modules/channel_imap/acl.js +10 -0
- package/dist/modules/channel_imap/acl.js.map +7 -0
- package/dist/modules/channel_imap/di.js +23 -0
- package/dist/modules/channel_imap/di.js.map +7 -0
- package/dist/modules/channel_imap/index.js +9 -0
- package/dist/modules/channel_imap/index.js.map +7 -0
- package/dist/modules/channel_imap/integration.js +135 -0
- package/dist/modules/channel_imap/integration.js.map +7 -0
- package/dist/modules/channel_imap/lib/adapter.js +291 -0
- package/dist/modules/channel_imap/lib/adapter.js.map +7 -0
- package/dist/modules/channel_imap/lib/capabilities.js +8 -0
- package/dist/modules/channel_imap/lib/capabilities.js.map +7 -0
- package/dist/modules/channel_imap/lib/convert-outbound.js +54 -0
- package/dist/modules/channel_imap/lib/convert-outbound.js.map +7 -0
- package/dist/modules/channel_imap/lib/credentials.js +104 -0
- package/dist/modules/channel_imap/lib/credentials.js.map +7 -0
- package/dist/modules/channel_imap/lib/health.js +39 -0
- package/dist/modules/channel_imap/lib/health.js.map +7 -0
- package/dist/modules/channel_imap/lib/host-pinning.js +34 -0
- package/dist/modules/channel_imap/lib/host-pinning.js.map +7 -0
- package/dist/modules/channel_imap/lib/imap-client.js +210 -0
- package/dist/modules/channel_imap/lib/imap-client.js.map +7 -0
- package/dist/modules/channel_imap/lib/normalize-inbound.js +19 -0
- package/dist/modules/channel_imap/lib/normalize-inbound.js.map +7 -0
- package/dist/modules/channel_imap/lib/smtp-client.js +113 -0
- package/dist/modules/channel_imap/lib/smtp-client.js.map +7 -0
- package/dist/modules/channel_imap/lib/transport.js +17 -0
- package/dist/modules/channel_imap/lib/transport.js.map +7 -0
- package/dist/modules/channel_imap/lib/validate-credentials.js +69 -0
- package/dist/modules/channel_imap/lib/validate-credentials.js.map +7 -0
- package/dist/modules/channel_imap/setup.js +25 -0
- package/dist/modules/channel_imap/setup.js.map +7 -0
- package/dist/modules/channel_imap/widgets/injection/connect/widget.client.js +337 -0
- package/dist/modules/channel_imap/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_imap/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_imap/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_imap/widgets/injection-table.js +14 -0
- package/dist/modules/channel_imap/widgets/injection-table.js.map +7 -0
- package/jest.config.cjs +34 -0
- package/package.json +99 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-001.spec.ts +80 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-002.spec.ts +28 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-003.spec.ts +23 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-021.spec.ts +40 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-022.spec.ts +38 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-023.spec.ts +31 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-024.spec.ts +27 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-025.spec.ts +23 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-026.spec.ts +18 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-027.spec.ts +18 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-028.spec.ts +19 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-029.spec.ts +72 -0
- package/src/modules/channel_imap/__integration__/TC-CHANNEL-EMAIL-030.spec.ts +19 -0
- package/src/modules/channel_imap/acl.ts +6 -0
- package/src/modules/channel_imap/di.ts +26 -0
- package/src/modules/channel_imap/index.ts +6 -0
- package/src/modules/channel_imap/integration.ts +131 -0
- package/src/modules/channel_imap/lib/__tests__/adapter.test.ts +499 -0
- package/src/modules/channel_imap/lib/__tests__/convert-outbound.test.ts +73 -0
- package/src/modules/channel_imap/lib/__tests__/credentials.test.ts +154 -0
- package/src/modules/channel_imap/lib/__tests__/host-pinning.test.ts +68 -0
- package/src/modules/channel_imap/lib/__tests__/imap-client.test.ts +180 -0
- package/src/modules/channel_imap/lib/__tests__/normalize-inbound.test.ts +126 -0
- package/src/modules/channel_imap/lib/__tests__/transport.test.ts +68 -0
- package/src/modules/channel_imap/lib/__tests__/validate-credentials.test.ts +156 -0
- package/src/modules/channel_imap/lib/adapter.ts +451 -0
- package/src/modules/channel_imap/lib/capabilities.ts +16 -0
- package/src/modules/channel_imap/lib/convert-outbound.ts +79 -0
- package/src/modules/channel_imap/lib/credentials.ts +172 -0
- package/src/modules/channel_imap/lib/health.ts +70 -0
- package/src/modules/channel_imap/lib/host-pinning.ts +59 -0
- package/src/modules/channel_imap/lib/imap-client.ts +382 -0
- package/src/modules/channel_imap/lib/normalize-inbound.ts +47 -0
- package/src/modules/channel_imap/lib/smtp-client.ts +214 -0
- package/src/modules/channel_imap/lib/transport.ts +37 -0
- package/src/modules/channel_imap/lib/validate-credentials.ts +98 -0
- package/src/modules/channel_imap/setup.ts +34 -0
- package/src/modules/channel_imap/widgets/injection/connect/widget.client.tsx +359 -0
- package/src/modules/channel_imap/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_imap/widgets/injection-table.ts +12 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
const base = require('../../jest.config.base.cjs')
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
...base,
|
|
6
|
+
testEnvironment: 'node',
|
|
7
|
+
watchman: false,
|
|
8
|
+
rootDir: '.',
|
|
9
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
10
|
+
moduleNameMapper: {
|
|
11
|
+
'^@open-mercato/channel-imap/(.*)$': '<rootDir>/src/$1',
|
|
12
|
+
'^@open-mercato/core/(.*)$': '<rootDir>/../core/src/$1',
|
|
13
|
+
'^@open-mercato/shared/(.*)$': '<rootDir>/../shared/src/$1',
|
|
14
|
+
'^@open-mercato/queue/(.*)$': '<rootDir>/../queue/src/$1',
|
|
15
|
+
'^@open-mercato/ui/(.*)$': '<rootDir>/../ui/src/$1',
|
|
16
|
+
},
|
|
17
|
+
transform: {
|
|
18
|
+
'^.+\\.(t|j)sx?$': [
|
|
19
|
+
'<rootDir>/../../scripts/jest-mikroorm-transformer.cjs',
|
|
20
|
+
{
|
|
21
|
+
tsconfig: {
|
|
22
|
+
jsx: 'react-jsx',
|
|
23
|
+
rootDir: '.',
|
|
24
|
+
ignoreDeprecations: '6.0',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
transformIgnorePatterns: [
|
|
30
|
+
'node_modules/(?!(@mikro-orm|kysely)/)',
|
|
31
|
+
],
|
|
32
|
+
testMatch: ['<rootDir>/src/**/__tests__/**/*.test.(ts|tsx)'],
|
|
33
|
+
passWithNoTests: true,
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@open-mercato/channel-imap",
|
|
3
|
+
"version": "0.6.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "node build.mjs",
|
|
8
|
+
"watch": "node watch.mjs",
|
|
9
|
+
"test": "jest --config jest.config.cjs",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./*.ts": {
|
|
15
|
+
"types": "./src/*.ts",
|
|
16
|
+
"default": "./dist/*.js"
|
|
17
|
+
},
|
|
18
|
+
"./*.tsx": {
|
|
19
|
+
"types": "./src/*.tsx",
|
|
20
|
+
"default": "./dist/*.js"
|
|
21
|
+
},
|
|
22
|
+
"./*.json": "./src/*.json",
|
|
23
|
+
"./*": {
|
|
24
|
+
"types": [
|
|
25
|
+
"./src/*.ts",
|
|
26
|
+
"./src/*.tsx"
|
|
27
|
+
],
|
|
28
|
+
"default": "./dist/*.js"
|
|
29
|
+
},
|
|
30
|
+
"./*/*.json": "./src/*/*.json",
|
|
31
|
+
"./*/*": {
|
|
32
|
+
"types": [
|
|
33
|
+
"./src/*/*.ts",
|
|
34
|
+
"./src/*/*.tsx"
|
|
35
|
+
],
|
|
36
|
+
"default": "./dist/*/*.js"
|
|
37
|
+
},
|
|
38
|
+
"./*/*/*.json": "./src/*/*/*.json",
|
|
39
|
+
"./*/*/*": {
|
|
40
|
+
"types": [
|
|
41
|
+
"./src/*/*/*.ts",
|
|
42
|
+
"./src/*/*/*.tsx"
|
|
43
|
+
],
|
|
44
|
+
"default": "./dist/*/*/*.js"
|
|
45
|
+
},
|
|
46
|
+
"./*/*/*/*.json": "./src/*/*/*/*.json",
|
|
47
|
+
"./*/*/*/*": {
|
|
48
|
+
"types": [
|
|
49
|
+
"./src/*/*/*/*.ts",
|
|
50
|
+
"./src/*/*/*/*.tsx"
|
|
51
|
+
],
|
|
52
|
+
"default": "./dist/*/*/*/*.js"
|
|
53
|
+
},
|
|
54
|
+
"./*/*/*/*/*.json": "./src/*/*/*/*/*.json",
|
|
55
|
+
"./*/*/*/*/*": {
|
|
56
|
+
"types": [
|
|
57
|
+
"./src/*/*/*/*/*.ts",
|
|
58
|
+
"./src/*/*/*/*/*.tsx"
|
|
59
|
+
],
|
|
60
|
+
"default": "./dist/*/*/*/*/*.js"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@open-mercato/core": "0.6.4",
|
|
65
|
+
"@open-mercato/queue": "0.6.4",
|
|
66
|
+
"@open-mercato/ui": "0.6.4",
|
|
67
|
+
"@types/mailparser": "^3.4.5",
|
|
68
|
+
"@types/nodemailer": "^8.0.0",
|
|
69
|
+
"imapflow": "^1.0.171",
|
|
70
|
+
"mailparser": "^3.7.1",
|
|
71
|
+
"nodemailer": "^8.0.10"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@mikro-orm/postgresql": "^7.0.14",
|
|
75
|
+
"@open-mercato/shared": "0.6.4",
|
|
76
|
+
"react": "^19.0.0",
|
|
77
|
+
"react-dom": "^19.0.0"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@open-mercato/shared": "0.6.4",
|
|
81
|
+
"@types/jest": "^30.0.0",
|
|
82
|
+
"@types/react": "^19.2.17",
|
|
83
|
+
"@types/react-dom": "^19.2.3",
|
|
84
|
+
"esbuild": "^0.28.0",
|
|
85
|
+
"glob": "^13.0.6",
|
|
86
|
+
"jest": "^30.4.2",
|
|
87
|
+
"react": "19.2.7",
|
|
88
|
+
"react-dom": "19.2.7",
|
|
89
|
+
"ts-jest": "^29.4.11"
|
|
90
|
+
},
|
|
91
|
+
"publishConfig": {
|
|
92
|
+
"access": "public"
|
|
93
|
+
},
|
|
94
|
+
"repository": {
|
|
95
|
+
"type": "git",
|
|
96
|
+
"url": "https://github.com/open-mercato/open-mercato",
|
|
97
|
+
"directory": "packages/channel-imap"
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { metadata } from './modules/channel_imap/index'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
import { readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* TC-CHANNEL-EMAIL-001 — IMAP provider visible to the hub's per-user channel API.
|
|
8
|
+
*
|
|
9
|
+
* After Slice 3e registers the `imap` adapter inside `@open-mercato/channel-imap`,
|
|
10
|
+
* the per-user channel routes must accept `providerKey: 'imap'` as a known provider.
|
|
11
|
+
* Without a live IMAP test server we exercise the routing-and-validation surface only;
|
|
12
|
+
* actual credential validation against a real IMAP host is covered by the unit tests
|
|
13
|
+
* in `lib/__tests__/validate-credentials.test.ts`.
|
|
14
|
+
*
|
|
15
|
+
* The route should NOT 404 the provider (proves registration), and should NOT 500
|
|
16
|
+
* (proves the request reaches the adapter and rejects cleanly when no live server
|
|
17
|
+
* is reachable).
|
|
18
|
+
*/
|
|
19
|
+
test.describe('TC-CHANNEL-EMAIL-001: IMAP provider registration', () => {
|
|
20
|
+
test('POST connect/credentials with providerKey=imap reaches the adapter', async ({ request }) => {
|
|
21
|
+
const token = await getAuthToken(request)
|
|
22
|
+
const response = await apiRequest(
|
|
23
|
+
request,
|
|
24
|
+
'POST',
|
|
25
|
+
'/api/communication_channels/channels/connect/credentials',
|
|
26
|
+
{
|
|
27
|
+
token,
|
|
28
|
+
data: {
|
|
29
|
+
providerKey: 'imap',
|
|
30
|
+
displayName: 'IMAP — integration test',
|
|
31
|
+
credentials: {
|
|
32
|
+
imapHost: 'invalid.test.example',
|
|
33
|
+
imapPort: 993,
|
|
34
|
+
imapTls: 'tls',
|
|
35
|
+
imapUser: 'fake@example.test',
|
|
36
|
+
imapPassword: 'wrong-password',
|
|
37
|
+
smtpHost: 'invalid.test.example',
|
|
38
|
+
smtpPort: 465,
|
|
39
|
+
smtpTls: 'tls',
|
|
40
|
+
smtpUser: 'fake@example.test',
|
|
41
|
+
smtpPassword: 'wrong-password',
|
|
42
|
+
fromAddress: 'fake@example.test',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
expect(response.status(), 'route should not 5xx').toBeLessThan(500)
|
|
48
|
+
// 401 (no auth seeded) / 422 (validation failure surfaced via createCrudFormError) /
|
|
49
|
+
// 502 ish — but never 404, which would indicate the provider isn't registered.
|
|
50
|
+
expect(response.status(), 'IMAP provider should be registered').not.toBe(404)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('POST connect/credentials with providerKey=imap and malformed credentials returns 422', async ({ request }) => {
|
|
54
|
+
const token = await getAuthToken(request)
|
|
55
|
+
const response = await apiRequest(
|
|
56
|
+
request,
|
|
57
|
+
'POST',
|
|
58
|
+
'/api/communication_channels/channels/connect/credentials',
|
|
59
|
+
{
|
|
60
|
+
token,
|
|
61
|
+
data: {
|
|
62
|
+
providerKey: 'imap',
|
|
63
|
+
displayName: 'IMAP — malformed',
|
|
64
|
+
credentials: {
|
|
65
|
+
// Missing required imap/smtp fields entirely.
|
|
66
|
+
fromAddress: 'not-an-email',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
expect(response.status()).toBeLessThan(500)
|
|
72
|
+
expect([401, 422, 400]).toContain(response.status())
|
|
73
|
+
if (response.status() === 422) {
|
|
74
|
+
// The route surfaces credential-validation failures as { error, fieldErrors }
|
|
75
|
+
// (see api/post/channels/connect/credentials/route.ts).
|
|
76
|
+
const body = await readJsonSafe<{ error?: string; fieldErrors?: Record<string, string> }>(response)
|
|
77
|
+
expect(body?.fieldErrors ?? body?.error).toBeTruthy()
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TC-CHANNEL-EMAIL-002 — IMAP webhook endpoint stays disabled.
|
|
7
|
+
*
|
|
8
|
+
* IMAP has no webhook flow; the polling worker drives inbound. We still expose
|
|
9
|
+
* the standard `/api/communication_channels/webhook/[provider]` URL because the
|
|
10
|
+
* hub's route is generic. The IMAP adapter's `verifyWebhook` returns an
|
|
11
|
+
* `eventType: 'other'` event so the route MUST respond 2xx (not handled) instead
|
|
12
|
+
* of 5xx-ing or 404-ing.
|
|
13
|
+
*/
|
|
14
|
+
test.describe('TC-CHANNEL-EMAIL-002: IMAP webhook is a no-op', () => {
|
|
15
|
+
test('POST /api/communication_channels/webhook/imap does not 5xx', async ({ request }) => {
|
|
16
|
+
const token = await getAuthToken(request)
|
|
17
|
+
const response = await apiRequest(
|
|
18
|
+
request,
|
|
19
|
+
'POST',
|
|
20
|
+
'/api/communication_channels/webhook/imap',
|
|
21
|
+
{
|
|
22
|
+
token,
|
|
23
|
+
data: { ping: true },
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
expect(response.status(), 'webhook route should not 5xx').toBeLessThan(500)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TC-CHANNEL-EMAIL-003 — Profile page renders with the IMAP provider installed.
|
|
7
|
+
*
|
|
8
|
+
* Confirms `@open-mercato/channel-imap` does not break the per-user profile page
|
|
9
|
+
* (`/backend/profile/communication-channels`). Empty-state still renders even
|
|
10
|
+
* when the IMAP provider is the only one registered.
|
|
11
|
+
*/
|
|
12
|
+
test.describe('TC-CHANNEL-EMAIL-003: profile page with IMAP provider installed', () => {
|
|
13
|
+
test('GET /backend/profile/communication-channels does not 5xx', async ({ request }) => {
|
|
14
|
+
const token = await getAuthToken(request)
|
|
15
|
+
const response = await apiRequest(
|
|
16
|
+
request,
|
|
17
|
+
'GET',
|
|
18
|
+
'/backend/profile/communication-channels',
|
|
19
|
+
{ token },
|
|
20
|
+
)
|
|
21
|
+
expect(response.status(), 'profile page should not 5xx').toBeLessThan(500)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TC-CHANNEL-EMAIL-021 — IMAP zero-history bootstrap
|
|
7
|
+
*
|
|
8
|
+
* Verifies that the polling worker surface exists and that the
|
|
9
|
+
* `/poll-now` operator endpoint is reachable. The full happy path —
|
|
10
|
+
* a freshly-connected channel persists `UIDVALIDITY` + `UIDNEXT` and
|
|
11
|
+
* fetches ZERO historical messages — is captured in the QA scenario
|
|
12
|
+
* markdown `TC-CHANNEL-EMAIL-021-imap-bootstrap.md`.
|
|
13
|
+
*
|
|
14
|
+
* Unit-level coverage of the bootstrap branch lives in
|
|
15
|
+
* `packages/channel-imap/.../lib/__tests__/adapter.test.ts`
|
|
16
|
+
* (`fetchHistory` describe block, "bootstrap" case).
|
|
17
|
+
*/
|
|
18
|
+
test.describe('TC-CHANNEL-EMAIL-021: IMAP bootstrap', () => {
|
|
19
|
+
test('poll-now endpoint requires authentication', async ({ request }) => {
|
|
20
|
+
const response = await apiRequest(
|
|
21
|
+
request,
|
|
22
|
+
'POST',
|
|
23
|
+
'/api/communication_channels/channels/00000000-0000-4000-8000-000000000021/poll-now',
|
|
24
|
+
// Intentionally empty token — this assertion is the 401 unauth path.
|
|
25
|
+
{ token: '' },
|
|
26
|
+
)
|
|
27
|
+
expect(response.status()).toBe(401)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('poll-now endpoint returns 400 for invalid UUID', async ({ request }) => {
|
|
31
|
+
const token = await getAuthToken(request)
|
|
32
|
+
const response = await apiRequest(
|
|
33
|
+
request,
|
|
34
|
+
'POST',
|
|
35
|
+
'/api/communication_channels/channels/not-a-uuid/poll-now',
|
|
36
|
+
{ token },
|
|
37
|
+
)
|
|
38
|
+
expect(response.status()).toBe(400)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TC-CHANNEL-EMAIL-022 — IMAP incremental ingest within 90s
|
|
7
|
+
*
|
|
8
|
+
* Verifies the channels-list endpoint is reachable (which proves the
|
|
9
|
+
* poll-tick scheduler + per-channel polling pipeline is wired). Full
|
|
10
|
+
* E2E (new mail arrives → polling worker picks it up within 60s tick →
|
|
11
|
+
* ingest-inbound-message creates a CRM interaction within 90s) is in
|
|
12
|
+
* the QA scenario markdown `TC-CHANNEL-EMAIL-022-imap-incremental.md`.
|
|
13
|
+
*/
|
|
14
|
+
test.describe('TC-CHANNEL-EMAIL-022: IMAP incremental polling', () => {
|
|
15
|
+
test('me/channels endpoint requires authentication', async ({ request }) => {
|
|
16
|
+
// Intentionally empty token — this assertion is the 401 unauth path.
|
|
17
|
+
const response = await apiRequest(
|
|
18
|
+
request,
|
|
19
|
+
'GET',
|
|
20
|
+
'/api/communication_channels/me/channels',
|
|
21
|
+
{ token: '' },
|
|
22
|
+
)
|
|
23
|
+
expect(response.status()).toBe(401)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('me/channels returns items array when authenticated', async ({ request }) => {
|
|
27
|
+
const token = await getAuthToken(request)
|
|
28
|
+
const response = await apiRequest(
|
|
29
|
+
request,
|
|
30
|
+
'GET',
|
|
31
|
+
'/api/communication_channels/me/channels',
|
|
32
|
+
{ token },
|
|
33
|
+
)
|
|
34
|
+
expect(response.status()).toBe(200)
|
|
35
|
+
const body = (await response.json()) as { items?: unknown }
|
|
36
|
+
expect(Array.isArray(body.items)).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TC-CHANNEL-EMAIL-023 — Threading via References token
|
|
6
|
+
*
|
|
7
|
+
* Smoke: the layered thread matcher prefers `token-references`
|
|
8
|
+
* (highest-confidence strategy) when an inbound message carries our
|
|
9
|
+
* synthetic `<om_TOKEN@open-mercato.invalid>` Message-ID in
|
|
10
|
+
* `References`. The 5-strategy fallthrough order is unit-tested in
|
|
11
|
+
* `packages/core/.../lib/__tests__/thread-matcher.test.ts`; the
|
|
12
|
+
* end-to-end "send → reply preserves References → CRM threads back"
|
|
13
|
+
* path is in the QA scenario markdown.
|
|
14
|
+
*
|
|
15
|
+
* Webhook-less providers have no public endpoint to assert against,
|
|
16
|
+
* so this smoke test is intentionally minimal — it just confirms the
|
|
17
|
+
* platform's send-as-user route is reachable (used by the outbound
|
|
18
|
+
* test setup in the QA scenario).
|
|
19
|
+
*/
|
|
20
|
+
test.describe('TC-CHANNEL-EMAIL-023: References-token threading', () => {
|
|
21
|
+
test('send-as-user endpoint requires authentication', async ({ request }) => {
|
|
22
|
+
const response = await apiRequest(
|
|
23
|
+
request,
|
|
24
|
+
'POST',
|
|
25
|
+
'/api/communication_channels/send-as-user',
|
|
26
|
+
// Intentionally empty token — this test asserts the 401 unauth path.
|
|
27
|
+
{ token: '', data: { channelId: '00000000-0000-4000-8000-000000000023' } },
|
|
28
|
+
)
|
|
29
|
+
expect(response.status()).toBe(401)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TC-CHANNEL-EMAIL-024 — Threading via body footer
|
|
6
|
+
*
|
|
7
|
+
* When the recipient's MUA strips RFC5322 References on reply, our
|
|
8
|
+
* hidden body-footer marker (`<span style="display:none">[OM:TOKEN]</span>`
|
|
9
|
+
* for HTML; `[OM:TOKEN]` bracketed marker for plain text) still
|
|
10
|
+
* survives quoting and the layered matcher's `token-body` strategy
|
|
11
|
+
* threads the reply correctly.
|
|
12
|
+
*
|
|
13
|
+
* Full E2E in the QA scenario markdown. This smoke test confirms the
|
|
14
|
+
* send-as-user endpoint exists (same shape as TC-023).
|
|
15
|
+
*/
|
|
16
|
+
test.describe('TC-CHANNEL-EMAIL-024: Body-footer threading', () => {
|
|
17
|
+
test('send-as-user endpoint exists at the expected path', async ({ request }) => {
|
|
18
|
+
const response = await apiRequest(
|
|
19
|
+
request,
|
|
20
|
+
'POST',
|
|
21
|
+
'/api/communication_channels/send-as-user',
|
|
22
|
+
// Intentionally empty token — the 401 / 400 branch is what we assert.
|
|
23
|
+
{ token: '', data: {} },
|
|
24
|
+
)
|
|
25
|
+
expect([400, 401]).toContain(response.status())
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TC-CHANNEL-EMAIL-025 — JWZ-headers fallback threading
|
|
5
|
+
*
|
|
6
|
+
* Verifies the `jwz-headers` strategy (Spec B § thread-matcher,
|
|
7
|
+
* confidence: medium) — when an inbound reply has no `om_*` token
|
|
8
|
+
* but carries `In-Reply-To` / `References` pointing at one of our
|
|
9
|
+
* outbound `Message-Id`s, the matcher walks the conventional JWZ
|
|
10
|
+
* algorithm against the existing `external_messages` table.
|
|
11
|
+
*
|
|
12
|
+
* This path is exercised end-to-end by the QA scenario markdown
|
|
13
|
+
* `TC-CHANNEL-EMAIL-025-jwz-fallback.md`. The unit-level coverage
|
|
14
|
+
* lives in `packages/core/.../lib/__tests__/thread-matcher.test.ts`
|
|
15
|
+
* (Strategy 3 — jwz-headers describe block).
|
|
16
|
+
*
|
|
17
|
+
* No additional API surface to assert at the integration layer
|
|
18
|
+
* beyond the smoke tests already in TC-023/024 — this spec file
|
|
19
|
+
* documents the existence of the JWZ pathway for QA-tracking purposes.
|
|
20
|
+
*/
|
|
21
|
+
test.describe('TC-CHANNEL-EMAIL-025: JWZ-headers fallback', () => {
|
|
22
|
+
test.skip('behavioral coverage: thread-matcher.test.ts Strategy 3 (jwz-headers). Playwright E2E is infeasible — provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).', () => {})
|
|
23
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TC-CHANNEL-EMAIL-026 — Subject + participants fallback threading
|
|
5
|
+
*
|
|
6
|
+
* Last-ditch matcher strategy: when neither `om_*` token nor JWZ
|
|
7
|
+
* headers are present, the matcher normalises the inbound subject
|
|
8
|
+
* (`Re:`, `Fwd:`, `[EXTERNAL]` stripped) and checks for an existing
|
|
9
|
+
* thread whose participants overlap >= 50% with the inbound's
|
|
10
|
+
* sender/to/cc. Confidence: low.
|
|
11
|
+
*
|
|
12
|
+
* Unit-tested in `packages/core/.../lib/__tests__/thread-matcher.test.ts`
|
|
13
|
+
* (Strategy 4 — subject-participants). End-to-end is the QA scenario
|
|
14
|
+
* markdown.
|
|
15
|
+
*/
|
|
16
|
+
test.describe('TC-CHANNEL-EMAIL-026: Subject + participants fallback', () => {
|
|
17
|
+
test.skip('behavioral coverage: thread-matcher.test.ts Strategy 4 (subject + participants). Playwright E2E is infeasible — provider mock seams are process-local (see TC-CHANNEL-EMAIL-031 / TC-CRM-EMAIL-001).', () => {})
|
|
18
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TC-CHANNEL-EMAIL-027 — Auto-recovery from `status='error'`
|
|
5
|
+
*
|
|
6
|
+
* Spec B § Phase B5 — `poll-tick.ts` enumerates two channel pools per
|
|
7
|
+
* tick: (a) `status='connected'` and due for polling, (b) `status='error'`
|
|
8
|
+
* whose `lastFailureAt` is older than `OM_CHANNEL_AUTO_RECOVER_MINUTES`
|
|
9
|
+
* (default 30). A successful poll in pool (b) flips status back to
|
|
10
|
+
* 'connected' via `poll-channel.ts`.
|
|
11
|
+
*
|
|
12
|
+
* Unit-tested in `packages/core/.../workers/__tests__/poll-tick.test.ts`
|
|
13
|
+
* (auto-recovery describe block). Full E2E (force a transient error →
|
|
14
|
+
* fast-forward 30 min → next tick recovers) is in the QA scenario.
|
|
15
|
+
*/
|
|
16
|
+
test.describe('TC-CHANNEL-EMAIL-027: Auto-recovery sweep', () => {
|
|
17
|
+
test.skip('behavioral coverage: workers/__tests__/poll-tick.test.ts (auto-recovery sweep). Playwright E2E is infeasible — provider mock seams are process-local + needs scheduler fast-forward.', () => {})
|
|
18
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TC-CHANNEL-EMAIL-028 — Malformed MIME → dead-letter, cursor advances
|
|
5
|
+
*
|
|
6
|
+
* Spec B § Phase B4 — `poll-channel.ts` classifies per-message ingest
|
|
7
|
+
* failures: transient failures abort the loop without advancing the
|
|
8
|
+
* cursor (idempotent retry on next tick), permanent failures write the
|
|
9
|
+
* raw MIME blob + error metadata to `ChannelIngestDeadLetter`
|
|
10
|
+
* (encrypted at rest via `defaultEncryptionMaps`) and the cursor
|
|
11
|
+
* advances anyway so the bad blob never re-stalls the channel.
|
|
12
|
+
*
|
|
13
|
+
* Unit-tested via the `ChannelIngestDeadLetter row shape (Spec B § B4)`
|
|
14
|
+
* describe in `ingest-inbound-message.test.ts` plus the implementation
|
|
15
|
+
* paths in `poll-channel.test.ts`. Full E2E in the QA scenario.
|
|
16
|
+
*/
|
|
17
|
+
test.describe('TC-CHANNEL-EMAIL-028: Malformed MIME → dead-letter', () => {
|
|
18
|
+
test.skip('behavioral coverage: workers/__tests__/poll-channel.test.ts (permanent ingest failure → dead-letter + cursor advances; transient → cursor held). Playwright E2E is infeasible — provider mock seams are process-local.', () => {})
|
|
19
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { getAuthToken } from '@open-mercato/core/helpers/integration/authFixtures'
|
|
3
|
+
import { apiRequest } from '@open-mercato/core/helpers/integration/api'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TC-CHANNEL-EMAIL-029 — operator-triggered backlog import (Spec B § Phase B6).
|
|
7
|
+
*
|
|
8
|
+
* Verifies that the `/api/communication_channels/channels/{id}/import-history`
|
|
9
|
+
* route is reachable, requires authentication, validates the body, and 404s
|
|
10
|
+
* for non-existent / not-owned channels (the access-control branch).
|
|
11
|
+
*
|
|
12
|
+
* Concurrency guard (429), the ProgressJob lifecycle, and the IMAP SEARCH +
|
|
13
|
+
* fetch loop are exercised by the unit-test layer:
|
|
14
|
+
* - packages/core/src/modules/communication_channels/commands/__tests__/queue-import-history.test.ts
|
|
15
|
+
* - packages/core/src/modules/communication_channels/workers/__tests__/channel-import-history.test.ts
|
|
16
|
+
* - packages/channel-imap/src/modules/channel_imap/lib/__tests__/adapter.test.ts (importHistory cases)
|
|
17
|
+
*
|
|
18
|
+
* The full end-to-end (connect a mailbox → POST /import-history → ProgressJob
|
|
19
|
+
* completes → imported messages visible on Person timeline) is captured in
|
|
20
|
+
* `.ai/qa/scenarios/TC-CHANNEL-EMAIL-029-import-history.md` for manual QA.
|
|
21
|
+
*/
|
|
22
|
+
test.describe('TC-CHANNEL-EMAIL-029: import-history route wiring', () => {
|
|
23
|
+
const FAKE_CHANNEL_ID = '00000000-0000-0000-0000-000000000029'
|
|
24
|
+
|
|
25
|
+
test('rejects unauthenticated requests', async ({ request }) => {
|
|
26
|
+
const response = await apiRequest(
|
|
27
|
+
request,
|
|
28
|
+
'POST',
|
|
29
|
+
`/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
|
|
30
|
+
// Intentionally empty token — this test asserts the 401 unauth path.
|
|
31
|
+
{ token: '', data: { sinceDays: 14 } },
|
|
32
|
+
)
|
|
33
|
+
expect(response.status()).toBe(401)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('returns 400 on invalid channel id', async ({ request }) => {
|
|
37
|
+
const token = await getAuthToken(request)
|
|
38
|
+
const response = await apiRequest(
|
|
39
|
+
request,
|
|
40
|
+
'POST',
|
|
41
|
+
'/api/communication_channels/channels/not-a-uuid/import-history',
|
|
42
|
+
{ token, data: { sinceDays: 14 } },
|
|
43
|
+
)
|
|
44
|
+
expect(response.status()).toBe(400)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('returns 400 when body fails Zod validation (sinceDays out of range)', async ({ request }) => {
|
|
48
|
+
const token = await getAuthToken(request)
|
|
49
|
+
const response = await apiRequest(
|
|
50
|
+
request,
|
|
51
|
+
'POST',
|
|
52
|
+
`/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
|
|
53
|
+
{ token, data: { sinceDays: 9999 } },
|
|
54
|
+
)
|
|
55
|
+
expect(response.status()).toBe(400)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('returns 404 for a channel the caller does not own', async ({ request }) => {
|
|
59
|
+
const token = await getAuthToken(request)
|
|
60
|
+
const response = await apiRequest(
|
|
61
|
+
request,
|
|
62
|
+
'POST',
|
|
63
|
+
`/api/communication_channels/channels/${FAKE_CHANNEL_ID}/import-history`,
|
|
64
|
+
{ token, data: { sinceDays: 14, maxMessages: 100 } },
|
|
65
|
+
)
|
|
66
|
+
// A caller without an organization scope is rejected with 400 ("No
|
|
67
|
+
// organization scope") before the channel lookup; a scoped caller reaches
|
|
68
|
+
// the channel-not-found branch (404, same shape as access-denied for parity).
|
|
69
|
+
expect(response.status(), 'route should not 5xx').toBeLessThan(500)
|
|
70
|
+
expect([400, 403, 404]).toContain(response.status())
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { test } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TC-CHANNEL-EMAIL-030 — Sent-folder dedup
|
|
5
|
+
*
|
|
6
|
+
* Spec B § Phase B3 — `ingest-inbound-message.ts` short-circuits when
|
|
7
|
+
* the inbound message's `messageId` matches an outbound
|
|
8
|
+
* `MessageChannelLink.channelMetadata.messageId` we already sent. This
|
|
9
|
+
* prevents IMAP polls of the Sent folder from creating duplicate
|
|
10
|
+
* inbound rows for every outbound the user sent.
|
|
11
|
+
*
|
|
12
|
+
* Contract unit-tested in `ingest-inbound-message.test.ts` (sent-folder
|
|
13
|
+
* dedup describe block). Full E2E in the QA scenario markdown — send
|
|
14
|
+
* an outbound, observe Sent folder surfaces it, confirm no duplicate
|
|
15
|
+
* `MessageChannelLink` row appears for the inbound direction.
|
|
16
|
+
*/
|
|
17
|
+
test.describe('TC-CHANNEL-EMAIL-030: Sent-folder dedup', () => {
|
|
18
|
+
test.skip('behavioral coverage: ingest-inbound-message.test.ts (sent-folder dedup contract + dedup → status=duplicate). Playwright E2E is infeasible — provider mock seams are process-local.', () => {})
|
|
19
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { asValue } from 'awilix'
|
|
2
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import {
|
|
4
|
+
hasChannelAdapter,
|
|
5
|
+
registerChannelAdapter,
|
|
6
|
+
} from '@open-mercato/core/modules/communication_channels/lib/adapter-registry-singleton'
|
|
7
|
+
import { getImapChannelAdapter } from './lib/adapter'
|
|
8
|
+
import { channelImapHealthCheck } from './lib/health'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Re-register the adapter on container creation as a safety net for runtime
|
|
12
|
+
* environments that bypass module setup (worker-only nodes, ad-hoc CLI). The
|
|
13
|
+
* underlying registry is process-wide so the registration is idempotent.
|
|
14
|
+
*/
|
|
15
|
+
export function register(container: AppContainer): void {
|
|
16
|
+
if (!hasChannelAdapter('imap')) {
|
|
17
|
+
registerChannelAdapter(getImapChannelAdapter())
|
|
18
|
+
}
|
|
19
|
+
container.register({
|
|
20
|
+
channelImapAdapter: asValue(getImapChannelAdapter()),
|
|
21
|
+
// Registered under the exact service name declared in `integration.ts`
|
|
22
|
+
// (`healthCheck.service`). Without this, the hub's `container.resolve(...)`
|
|
23
|
+
// throws and the channel reports permanently 'unhealthy'.
|
|
24
|
+
channelImapHealthCheck: asValue(channelImapHealthCheck),
|
|
25
|
+
})
|
|
26
|
+
}
|