@code-rag/core 0.1.0 → 0.1.1
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/package.json +4 -4
- package/dist/auth/audit-log.js.map +0 -1
- package/dist/auth/audit-log.test.d.ts +0 -1
- package/dist/auth/audit-log.test.js +0 -261
- package/dist/auth/audit-log.test.js.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/oidc-provider.js.map +0 -1
- package/dist/auth/oidc-provider.test.d.ts +0 -1
- package/dist/auth/oidc-provider.test.js +0 -520
- package/dist/auth/oidc-provider.test.js.map +0 -1
- package/dist/auth/rbac.js.map +0 -1
- package/dist/auth/rbac.test.d.ts +0 -1
- package/dist/auth/rbac.test.js +0 -224
- package/dist/auth/rbac.test.js.map +0 -1
- package/dist/auth/saml-provider.js.map +0 -1
- package/dist/auth/saml-provider.test.d.ts +0 -1
- package/dist/auth/saml-provider.test.js +0 -422
- package/dist/auth/saml-provider.test.js.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/auth/types.test.d.ts +0 -1
- package/dist/auth/types.test.js +0 -147
- package/dist/auth/types.test.js.map +0 -1
- package/dist/backlog/ab-reference-scanner.js.map +0 -1
- package/dist/backlog/ab-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/ab-reference-scanner.test.js +0 -83
- package/dist/backlog/ab-reference-scanner.test.js.map +0 -1
- package/dist/backlog/azure-devops-provider.js.map +0 -1
- package/dist/backlog/backlog-provider.js.map +0 -1
- package/dist/backlog/backlog-provider.test.d.ts +0 -1
- package/dist/backlog/backlog-provider.test.js +0 -426
- package/dist/backlog/backlog-provider.test.js.map +0 -1
- package/dist/backlog/clickup-provider.js.map +0 -1
- package/dist/backlog/clickup-provider.test.d.ts +0 -1
- package/dist/backlog/clickup-provider.test.js +0 -426
- package/dist/backlog/clickup-provider.test.js.map +0 -1
- package/dist/backlog/clickup-reference-scanner.js.map +0 -1
- package/dist/backlog/clickup-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/clickup-reference-scanner.test.js +0 -92
- package/dist/backlog/clickup-reference-scanner.test.js.map +0 -1
- package/dist/backlog/code-linker.js.map +0 -1
- package/dist/backlog/code-linker.test.d.ts +0 -1
- package/dist/backlog/code-linker.test.js +0 -325
- package/dist/backlog/code-linker.test.js.map +0 -1
- package/dist/backlog/index.js.map +0 -1
- package/dist/backlog/jira-provider.js.map +0 -1
- package/dist/backlog/jira-provider.test.d.ts +0 -1
- package/dist/backlog/jira-provider.test.js +0 -449
- package/dist/backlog/jira-provider.test.js.map +0 -1
- package/dist/backlog/jira-reference-scanner.js.map +0 -1
- package/dist/backlog/jira-reference-scanner.test.d.ts +0 -1
- package/dist/backlog/jira-reference-scanner.test.js +0 -127
- package/dist/backlog/jira-reference-scanner.test.js.map +0 -1
- package/dist/backlog/types.js.map +0 -1
- package/dist/chunker/ast-chunker.js.map +0 -1
- package/dist/chunker/ast-chunker.test.d.ts +0 -1
- package/dist/chunker/ast-chunker.test.js +0 -391
- package/dist/chunker/ast-chunker.test.js.map +0 -1
- package/dist/chunker/chunker.js.map +0 -1
- package/dist/chunker/index.js.map +0 -1
- package/dist/config/config-parser.js.map +0 -1
- package/dist/config/config-parser.test.d.ts +0 -1
- package/dist/config/config-parser.test.js +0 -699
- package/dist/config/config-parser.test.js.map +0 -1
- package/dist/docs/confluence-provider.js.map +0 -1
- package/dist/docs/confluence-provider.test.d.ts +0 -1
- package/dist/docs/confluence-provider.test.js +0 -765
- package/dist/docs/confluence-provider.test.js.map +0 -1
- package/dist/docs/index.js.map +0 -1
- package/dist/docs/sharepoint-provider.js.map +0 -1
- package/dist/docs/sharepoint-provider.test.d.ts +0 -1
- package/dist/docs/sharepoint-provider.test.js +0 -873
- package/dist/docs/sharepoint-provider.test.js.map +0 -1
- package/dist/embedding/bm25-index.js.map +0 -1
- package/dist/embedding/bm25-index.test.d.ts +0 -1
- package/dist/embedding/bm25-index.test.js +0 -289
- package/dist/embedding/bm25-index.test.js.map +0 -1
- package/dist/embedding/hybrid-search.js.map +0 -1
- package/dist/embedding/hybrid-search.test.d.ts +0 -1
- package/dist/embedding/hybrid-search.test.js +0 -266
- package/dist/embedding/hybrid-search.test.js.map +0 -1
- package/dist/embedding/index.js.map +0 -1
- package/dist/embedding/lancedb-store.js.map +0 -1
- package/dist/embedding/lancedb-store.test.d.ts +0 -1
- package/dist/embedding/lancedb-store.test.js +0 -268
- package/dist/embedding/lancedb-store.test.js.map +0 -1
- package/dist/embedding/model-lifecycle-manager.js.map +0 -1
- package/dist/embedding/model-lifecycle-manager.test.d.ts +0 -1
- package/dist/embedding/model-lifecycle-manager.test.js +0 -642
- package/dist/embedding/model-lifecycle-manager.test.js.map +0 -1
- package/dist/embedding/ollama-embedding-provider.js.map +0 -1
- package/dist/embedding/ollama-embedding-provider.test.d.ts +0 -1
- package/dist/embedding/ollama-embedding-provider.test.js +0 -198
- package/dist/embedding/ollama-embedding-provider.test.js.map +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.js.map +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.test.d.ts +0 -1
- package/dist/embedding/openai-compatible-embedding-provider.test.js +0 -456
- package/dist/embedding/openai-compatible-embedding-provider.test.js.map +0 -1
- package/dist/embedding/qdrant-store.js.map +0 -1
- package/dist/embedding/qdrant-store.test.d.ts +0 -1
- package/dist/embedding/qdrant-store.test.js +0 -359
- package/dist/embedding/qdrant-store.test.js.map +0 -1
- package/dist/enrichment/index.js.map +0 -1
- package/dist/enrichment/nl-enricher.js.map +0 -1
- package/dist/enrichment/nl-enricher.test.d.ts +0 -1
- package/dist/enrichment/nl-enricher.test.js +0 -154
- package/dist/enrichment/nl-enricher.test.js.map +0 -1
- package/dist/enrichment/ollama-client.js.map +0 -1
- package/dist/enrichment/ollama-client.test.d.ts +0 -1
- package/dist/enrichment/ollama-client.test.js +0 -129
- package/dist/enrichment/ollama-client.test.js.map +0 -1
- package/dist/git/git-client.js.map +0 -1
- package/dist/git/git-client.test.d.ts +0 -1
- package/dist/git/git-client.test.js +0 -200
- package/dist/git/git-client.test.js.map +0 -1
- package/dist/git/ignore-filter.js.map +0 -1
- package/dist/git/ignore-filter.test.d.ts +0 -1
- package/dist/git/ignore-filter.test.js +0 -87
- package/dist/git/ignore-filter.test.js.map +0 -1
- package/dist/git/index.js.map +0 -1
- package/dist/git/simple-git-client.js.map +0 -1
- package/dist/graph/cross-repo-resolver.js.map +0 -1
- package/dist/graph/cross-repo-resolver.test.d.ts +0 -1
- package/dist/graph/cross-repo-resolver.test.js +0 -548
- package/dist/graph/cross-repo-resolver.test.js.map +0 -1
- package/dist/graph/dependency-graph.js.map +0 -1
- package/dist/graph/dependency-graph.test.d.ts +0 -1
- package/dist/graph/dependency-graph.test.js +0 -276
- package/dist/graph/dependency-graph.test.js.map +0 -1
- package/dist/graph/graph-builder.js.map +0 -1
- package/dist/graph/graph-builder.test.d.ts +0 -1
- package/dist/graph/graph-builder.test.js +0 -178
- package/dist/graph/graph-builder.test.js.map +0 -1
- package/dist/graph/import-resolver.js.map +0 -1
- package/dist/graph/import-resolver.test.d.ts +0 -1
- package/dist/graph/import-resolver.test.js +0 -282
- package/dist/graph/import-resolver.test.js.map +0 -1
- package/dist/graph/index.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/indexer/file-scanner.js.map +0 -1
- package/dist/indexer/file-scanner.test.d.ts +0 -1
- package/dist/indexer/file-scanner.test.js +0 -110
- package/dist/indexer/file-scanner.test.js.map +0 -1
- package/dist/indexer/incremental-indexer.js.map +0 -1
- package/dist/indexer/incremental-indexer.test.d.ts +0 -1
- package/dist/indexer/incremental-indexer.test.js +0 -266
- package/dist/indexer/incremental-indexer.test.js.map +0 -1
- package/dist/indexer/index-check.js.map +0 -1
- package/dist/indexer/index-check.test.d.ts +0 -1
- package/dist/indexer/index-check.test.js +0 -100
- package/dist/indexer/index-check.test.js.map +0 -1
- package/dist/indexer/index-state.js.map +0 -1
- package/dist/indexer/index-state.test.d.ts +0 -1
- package/dist/indexer/index-state.test.js +0 -140
- package/dist/indexer/index-state.test.js.map +0 -1
- package/dist/indexer/index.js.map +0 -1
- package/dist/indexer/multi-repo-indexer.js.map +0 -1
- package/dist/indexer/multi-repo-indexer.test.d.ts +0 -1
- package/dist/indexer/multi-repo-indexer.test.js +0 -238
- package/dist/indexer/multi-repo-indexer.test.js.map +0 -1
- package/dist/parser/index.js.map +0 -1
- package/dist/parser/language-registry.js.map +0 -1
- package/dist/parser/language-registry.test.d.ts +0 -1
- package/dist/parser/language-registry.test.js +0 -225
- package/dist/parser/language-registry.test.js.map +0 -1
- package/dist/parser/markdown-parser.js.map +0 -1
- package/dist/parser/markdown-parser.test.d.ts +0 -1
- package/dist/parser/markdown-parser.test.js +0 -600
- package/dist/parser/markdown-parser.test.js.map +0 -1
- package/dist/parser/tree-sitter-parser.js.map +0 -1
- package/dist/retrieval/context-expander.js.map +0 -1
- package/dist/retrieval/context-expander.test.d.ts +0 -1
- package/dist/retrieval/context-expander.test.js +0 -339
- package/dist/retrieval/context-expander.test.js.map +0 -1
- package/dist/retrieval/cross-encoder-reranker.js.map +0 -1
- package/dist/retrieval/cross-encoder-reranker.test.d.ts +0 -1
- package/dist/retrieval/cross-encoder-reranker.test.js +0 -305
- package/dist/retrieval/cross-encoder-reranker.test.js.map +0 -1
- package/dist/retrieval/index.js.map +0 -1
- package/dist/retrieval/query-analyzer.js.map +0 -1
- package/dist/retrieval/query-analyzer.test.d.ts +0 -1
- package/dist/retrieval/query-analyzer.test.js +0 -236
- package/dist/retrieval/query-analyzer.test.js.map +0 -1
- package/dist/retrieval/token-budget.js.map +0 -1
- package/dist/retrieval/token-budget.test.d.ts +0 -1
- package/dist/retrieval/token-budget.test.js +0 -404
- package/dist/retrieval/token-budget.test.js.map +0 -1
- package/dist/storage/azure-blob-provider.js.map +0 -1
- package/dist/storage/azure-blob-provider.test.d.ts +0 -1
- package/dist/storage/azure-blob-provider.test.js +0 -250
- package/dist/storage/azure-blob-provider.test.js.map +0 -1
- package/dist/storage/gcs-provider.js.map +0 -1
- package/dist/storage/gcs-provider.test.d.ts +0 -1
- package/dist/storage/gcs-provider.test.js +0 -299
- package/dist/storage/gcs-provider.test.js.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/s3-provider.js.map +0 -1
- package/dist/storage/s3-provider.test.d.ts +0 -1
- package/dist/storage/s3-provider.test.js +0 -329
- package/dist/storage/s3-provider.test.js.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/types/chunk.js.map +0 -1
- package/dist/types/config.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/types/provider.js.map +0 -1
- package/dist/types/search.js.map +0 -1
|
@@ -1,873 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { SharePointProvider, SharePointError, extractTextFromDocx, extractTextFromPdf, } from './sharepoint-provider.js';
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Helpers
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
function createMockResponse(body, status = 200, statusText = 'OK') {
|
|
7
|
-
return {
|
|
8
|
-
ok: status >= 200 && status < 300,
|
|
9
|
-
status,
|
|
10
|
-
statusText,
|
|
11
|
-
json: () => Promise.resolve(body),
|
|
12
|
-
arrayBuffer: () => Promise.resolve(body),
|
|
13
|
-
headers: new Headers(),
|
|
14
|
-
redirected: false,
|
|
15
|
-
type: 'basic',
|
|
16
|
-
url: '',
|
|
17
|
-
clone: () => createMockResponse(body, status, statusText),
|
|
18
|
-
body: null,
|
|
19
|
-
bodyUsed: false,
|
|
20
|
-
blob: () => Promise.resolve(new Blob()),
|
|
21
|
-
formData: () => Promise.resolve(new FormData()),
|
|
22
|
-
text: () => Promise.resolve(JSON.stringify(body)),
|
|
23
|
-
bytes: () => Promise.resolve(new Uint8Array()),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
function createTokenResponse(expiresIn = 3600) {
|
|
27
|
-
return {
|
|
28
|
-
access_token: 'mock-access-token-abc123',
|
|
29
|
-
token_type: 'Bearer',
|
|
30
|
-
expires_in: expiresIn,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function createSitePage(overrides) {
|
|
34
|
-
return {
|
|
35
|
-
id: 'page-001',
|
|
36
|
-
title: 'Engineering Handbook',
|
|
37
|
-
webUrl: 'https://contoso.sharepoint.com/sites/eng/SitePages/handbook.aspx',
|
|
38
|
-
lastModifiedDateTime: '2026-02-20T10:00:00Z',
|
|
39
|
-
contentType: { name: 'Site Page' },
|
|
40
|
-
...(overrides ?? {}),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function createDriveItem(overrides) {
|
|
44
|
-
return {
|
|
45
|
-
id: 'item-001',
|
|
46
|
-
name: 'Architecture.docx',
|
|
47
|
-
file: {
|
|
48
|
-
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
49
|
-
},
|
|
50
|
-
size: 45000,
|
|
51
|
-
webUrl: 'https://contoso.sharepoint.com/sites/eng/Shared%20Documents/Architecture.docx',
|
|
52
|
-
lastModifiedDateTime: '2026-02-18T14:30:00Z',
|
|
53
|
-
parentReference: {
|
|
54
|
-
driveId: 'drive-001',
|
|
55
|
-
name: 'Documents',
|
|
56
|
-
},
|
|
57
|
-
...(overrides ?? {}),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
function createDrive(overrides) {
|
|
61
|
-
return {
|
|
62
|
-
id: 'drive-001',
|
|
63
|
-
name: 'Documents',
|
|
64
|
-
webUrl: 'https://contoso.sharepoint.com/sites/eng/Shared%20Documents',
|
|
65
|
-
...(overrides ?? {}),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const VALID_CONFIG = {
|
|
69
|
-
tenantId: 'tenant-abc-123',
|
|
70
|
-
clientId: 'client-def-456',
|
|
71
|
-
clientSecret: 'secret-ghi-789',
|
|
72
|
-
siteIds: ['site-001', 'site-002'],
|
|
73
|
-
libraryNames: ['Documents'],
|
|
74
|
-
maxPages: 25,
|
|
75
|
-
};
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Tests
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
describe('SharePointProvider', () => {
|
|
80
|
-
let provider;
|
|
81
|
-
let fetchSpy;
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
provider = new SharePointProvider();
|
|
84
|
-
fetchSpy = vi.fn();
|
|
85
|
-
vi.stubGlobal('fetch', fetchSpy);
|
|
86
|
-
});
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
vi.restoreAllMocks();
|
|
89
|
-
});
|
|
90
|
-
// --- name ---
|
|
91
|
-
it('should have name set to sharepoint', () => {
|
|
92
|
-
expect(provider.name).toBe('sharepoint');
|
|
93
|
-
});
|
|
94
|
-
// --- initialize ---
|
|
95
|
-
describe('initialize', () => {
|
|
96
|
-
it('should return ok on successful token acquisition', async () => {
|
|
97
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
98
|
-
const result = await provider.initialize(VALID_CONFIG);
|
|
99
|
-
expect(result.isOk()).toBe(true);
|
|
100
|
-
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
101
|
-
const url = fetchSpy.mock.calls[0][0];
|
|
102
|
-
expect(url).toContain('login.microsoftonline.com');
|
|
103
|
-
expect(url).toContain(VALID_CONFIG.tenantId);
|
|
104
|
-
expect(url).toContain('/oauth2/v2.0/token');
|
|
105
|
-
});
|
|
106
|
-
it('should send correct OAuth2 client credentials request', async () => {
|
|
107
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
108
|
-
await provider.initialize(VALID_CONFIG);
|
|
109
|
-
const callOptions = fetchSpy.mock.calls[0][1];
|
|
110
|
-
expect(callOptions.method).toBe('POST');
|
|
111
|
-
const contentType = callOptions.headers['Content-Type'];
|
|
112
|
-
expect(contentType).toBe('application/x-www-form-urlencoded');
|
|
113
|
-
const body = callOptions.body;
|
|
114
|
-
expect(body).toContain('client_id=client-def-456');
|
|
115
|
-
expect(body).toContain('client_secret=secret-ghi-789');
|
|
116
|
-
expect(body).toContain('grant_type=client_credentials');
|
|
117
|
-
expect(body).toContain('scope=https%3A%2F%2Fgraph.microsoft.com%2F.default');
|
|
118
|
-
});
|
|
119
|
-
it('should return err when tenantId is missing', async () => {
|
|
120
|
-
const result = await provider.initialize({
|
|
121
|
-
clientId: 'abc',
|
|
122
|
-
clientSecret: 'def',
|
|
123
|
-
});
|
|
124
|
-
expect(result.isErr()).toBe(true);
|
|
125
|
-
if (result.isErr()) {
|
|
126
|
-
expect(result.error).toBeInstanceOf(SharePointError);
|
|
127
|
-
expect(result.error.message).toContain('tenantId');
|
|
128
|
-
}
|
|
129
|
-
});
|
|
130
|
-
it('should return err when clientId is missing', async () => {
|
|
131
|
-
const result = await provider.initialize({
|
|
132
|
-
tenantId: 'abc',
|
|
133
|
-
clientSecret: 'def',
|
|
134
|
-
});
|
|
135
|
-
expect(result.isErr()).toBe(true);
|
|
136
|
-
if (result.isErr()) {
|
|
137
|
-
expect(result.error).toBeInstanceOf(SharePointError);
|
|
138
|
-
expect(result.error.message).toContain('clientId');
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
it('should return err when clientSecret is missing', async () => {
|
|
142
|
-
const result = await provider.initialize({
|
|
143
|
-
tenantId: 'abc',
|
|
144
|
-
clientId: 'def',
|
|
145
|
-
});
|
|
146
|
-
expect(result.isErr()).toBe(true);
|
|
147
|
-
if (result.isErr()) {
|
|
148
|
-
expect(result.error).toBeInstanceOf(SharePointError);
|
|
149
|
-
expect(result.error.message).toContain('clientSecret');
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
it('should return err when token acquisition fails with HTTP error', async () => {
|
|
153
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ error: 'invalid_client' }, 401, 'Unauthorized'));
|
|
154
|
-
const result = await provider.initialize(VALID_CONFIG);
|
|
155
|
-
expect(result.isErr()).toBe(true);
|
|
156
|
-
if (result.isErr()) {
|
|
157
|
-
expect(result.error).toBeInstanceOf(SharePointError);
|
|
158
|
-
expect(result.error.message).toContain('401');
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
it('should return err when token acquisition throws a network error', async () => {
|
|
162
|
-
fetchSpy.mockRejectedValueOnce(new Error('ECONNREFUSED'));
|
|
163
|
-
const result = await provider.initialize(VALID_CONFIG);
|
|
164
|
-
expect(result.isErr()).toBe(true);
|
|
165
|
-
if (result.isErr()) {
|
|
166
|
-
expect(result.error.message).toContain('ECONNREFUSED');
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
it('should use default maxPages when not specified', async () => {
|
|
170
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
171
|
-
const result = await provider.initialize({
|
|
172
|
-
tenantId: 'abc',
|
|
173
|
-
clientId: 'def',
|
|
174
|
-
clientSecret: 'ghi',
|
|
175
|
-
});
|
|
176
|
-
expect(result.isOk()).toBe(true);
|
|
177
|
-
});
|
|
178
|
-
it('should filter out non-string siteIds', async () => {
|
|
179
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
180
|
-
const result = await provider.initialize({
|
|
181
|
-
...VALID_CONFIG,
|
|
182
|
-
siteIds: ['valid-site', 123, null, 'another-site'],
|
|
183
|
-
});
|
|
184
|
-
expect(result.isOk()).toBe(true);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
// --- fetchPages ---
|
|
188
|
-
describe('fetchPages', () => {
|
|
189
|
-
async function initializeProvider() {
|
|
190
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
191
|
-
await provider.initialize(VALID_CONFIG);
|
|
192
|
-
}
|
|
193
|
-
it('should fetch pages from specified sites', async () => {
|
|
194
|
-
await initializeProvider();
|
|
195
|
-
// Pages response
|
|
196
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
197
|
-
value: [createSitePage()],
|
|
198
|
-
}));
|
|
199
|
-
// Page content (web parts)
|
|
200
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
201
|
-
value: [{ innerHtml: '<p>Welcome to the Engineering Handbook.</p>' }],
|
|
202
|
-
}));
|
|
203
|
-
const result = await provider.fetchPages(['site-001']);
|
|
204
|
-
expect(result.isOk()).toBe(true);
|
|
205
|
-
if (result.isOk()) {
|
|
206
|
-
expect(result.value).toHaveLength(1);
|
|
207
|
-
const page = result.value[0];
|
|
208
|
-
expect(page.id).toBe('page-001');
|
|
209
|
-
expect(page.title).toBe('Engineering Handbook');
|
|
210
|
-
expect(page.type).toBe('page');
|
|
211
|
-
expect(page.siteId).toBe('site-001');
|
|
212
|
-
expect(page.plainText).toContain('Welcome to the Engineering Handbook');
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
it('should handle pagination with @odata.nextLink', async () => {
|
|
216
|
-
await initializeProvider();
|
|
217
|
-
// First page of results
|
|
218
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
219
|
-
value: [createSitePage({ id: 'page-1', title: 'Page 1' })],
|
|
220
|
-
'@odata.nextLink': 'https://graph.microsoft.com/v1.0/sites/site-001/pages?$skiptoken=abc',
|
|
221
|
-
}));
|
|
222
|
-
// Second page of results
|
|
223
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
224
|
-
value: [createSitePage({ id: 'page-2', title: 'Page 2' })],
|
|
225
|
-
}));
|
|
226
|
-
// Content fetch for page-1
|
|
227
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
228
|
-
// Content fetch for page-2
|
|
229
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
230
|
-
const result = await provider.fetchPages(['site-001']);
|
|
231
|
-
expect(result.isOk()).toBe(true);
|
|
232
|
-
if (result.isOk()) {
|
|
233
|
-
expect(result.value).toHaveLength(2);
|
|
234
|
-
expect(result.value[0].id).toBe('page-1');
|
|
235
|
-
expect(result.value[1].id).toBe('page-2');
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
it('should return err when no site IDs are provided and none configured', async () => {
|
|
239
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
240
|
-
await provider.initialize({
|
|
241
|
-
tenantId: 'abc',
|
|
242
|
-
clientId: 'def',
|
|
243
|
-
clientSecret: 'ghi',
|
|
244
|
-
});
|
|
245
|
-
const result = await provider.fetchPages();
|
|
246
|
-
expect(result.isErr()).toBe(true);
|
|
247
|
-
if (result.isErr()) {
|
|
248
|
-
expect(result.error.message).toContain('No site IDs');
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
it('should use configured siteIds when none passed explicitly', async () => {
|
|
252
|
-
await initializeProvider();
|
|
253
|
-
// Pages for site-001
|
|
254
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createSitePage()] }));
|
|
255
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
256
|
-
// Pages for site-002
|
|
257
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
258
|
-
value: [createSitePage({ id: 'page-002', title: 'Site 2 Page' })],
|
|
259
|
-
}));
|
|
260
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
261
|
-
const result = await provider.fetchPages();
|
|
262
|
-
expect(result.isOk()).toBe(true);
|
|
263
|
-
if (result.isOk()) {
|
|
264
|
-
expect(result.value).toHaveLength(2);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
it('should return err on Graph API failure', async () => {
|
|
268
|
-
await initializeProvider();
|
|
269
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ error: { message: 'Forbidden' } }, 403, 'Forbidden'));
|
|
270
|
-
const result = await provider.fetchPages(['site-001']);
|
|
271
|
-
expect(result.isErr()).toBe(true);
|
|
272
|
-
if (result.isErr()) {
|
|
273
|
-
expect(result.error.message).toContain('403');
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
it('should return empty array when no pages exist', async () => {
|
|
277
|
-
await initializeProvider();
|
|
278
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
279
|
-
const result = await provider.fetchPages(['site-001']);
|
|
280
|
-
expect(result.isOk()).toBe(true);
|
|
281
|
-
if (result.isOk()) {
|
|
282
|
-
expect(result.value).toEqual([]);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
it('should handle page content fetch failure gracefully', async () => {
|
|
286
|
-
await initializeProvider();
|
|
287
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createSitePage()] }));
|
|
288
|
-
// Content fetch fails
|
|
289
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
|
|
290
|
-
const result = await provider.fetchPages(['site-001']);
|
|
291
|
-
expect(result.isOk()).toBe(true);
|
|
292
|
-
if (result.isOk()) {
|
|
293
|
-
expect(result.value).toHaveLength(1);
|
|
294
|
-
expect(result.value[0].plainText).toBe('');
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
it('should combine web part HTML content', async () => {
|
|
298
|
-
await initializeProvider();
|
|
299
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createSitePage()] }));
|
|
300
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
301
|
-
value: [
|
|
302
|
-
{ innerHtml: '<p>Part 1</p>' },
|
|
303
|
-
{ data: { innerHTML: '<p>Part 2</p>' } },
|
|
304
|
-
],
|
|
305
|
-
}));
|
|
306
|
-
const result = await provider.fetchPages(['site-001']);
|
|
307
|
-
expect(result.isOk()).toBe(true);
|
|
308
|
-
if (result.isOk()) {
|
|
309
|
-
expect(result.value[0].plainText).toContain('Part 1');
|
|
310
|
-
expect(result.value[0].plainText).toContain('Part 2');
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
it('should return err on network error', async () => {
|
|
314
|
-
await initializeProvider();
|
|
315
|
-
fetchSpy.mockRejectedValueOnce(new Error('Network error'));
|
|
316
|
-
const result = await provider.fetchPages(['site-001']);
|
|
317
|
-
expect(result.isErr()).toBe(true);
|
|
318
|
-
if (result.isErr()) {
|
|
319
|
-
expect(result.error.message).toContain('Network error');
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
// --- fetchDocuments ---
|
|
324
|
-
describe('fetchDocuments', () => {
|
|
325
|
-
async function initializeProvider() {
|
|
326
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
327
|
-
await provider.initialize(VALID_CONFIG);
|
|
328
|
-
}
|
|
329
|
-
it('should fetch documents from a site library', async () => {
|
|
330
|
-
await initializeProvider();
|
|
331
|
-
// Drives response
|
|
332
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
333
|
-
// Drive items response
|
|
334
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDriveItem()] }));
|
|
335
|
-
// File download (docx content)
|
|
336
|
-
const emptyZip = new ArrayBuffer(0);
|
|
337
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(emptyZip));
|
|
338
|
-
const result = await provider.fetchDocuments('site-001');
|
|
339
|
-
expect(result.isOk()).toBe(true);
|
|
340
|
-
if (result.isOk()) {
|
|
341
|
-
expect(result.value).toHaveLength(1);
|
|
342
|
-
const doc = result.value[0];
|
|
343
|
-
expect(doc.id).toBe('item-001');
|
|
344
|
-
expect(doc.name).toBe('Architecture.docx');
|
|
345
|
-
expect(doc.type).toBe('document');
|
|
346
|
-
expect(doc.siteId).toBe('site-001');
|
|
347
|
-
expect(doc.libraryName).toBe('Documents');
|
|
348
|
-
expect(doc.mimeType).toContain('wordprocessingml');
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
it('should filter by library name', async () => {
|
|
352
|
-
await initializeProvider();
|
|
353
|
-
// Drives: multiple libraries
|
|
354
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
355
|
-
value: [
|
|
356
|
-
createDrive({ id: 'drive-001', name: 'Documents' }),
|
|
357
|
-
createDrive({ id: 'drive-002', name: 'Archives' }),
|
|
358
|
-
],
|
|
359
|
-
}));
|
|
360
|
-
// Only "Documents" drive should be queried
|
|
361
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
362
|
-
const result = await provider.fetchDocuments('site-001', 'Documents');
|
|
363
|
-
expect(result.isOk()).toBe(true);
|
|
364
|
-
// Should only fetch items from the matching drive
|
|
365
|
-
// The second fetchSpy call is for drive items of "Documents" only
|
|
366
|
-
expect(fetchSpy).toHaveBeenCalledTimes(3); // token + drives + drive items
|
|
367
|
-
});
|
|
368
|
-
it('should skip unsupported file types', async () => {
|
|
369
|
-
await initializeProvider();
|
|
370
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
371
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
372
|
-
value: [
|
|
373
|
-
createDriveItem({
|
|
374
|
-
id: 'img-001',
|
|
375
|
-
name: 'screenshot.png',
|
|
376
|
-
file: { mimeType: 'image/png' },
|
|
377
|
-
}),
|
|
378
|
-
createDriveItem({
|
|
379
|
-
id: 'doc-001',
|
|
380
|
-
name: 'report.docx',
|
|
381
|
-
file: {
|
|
382
|
-
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
383
|
-
},
|
|
384
|
-
}),
|
|
385
|
-
],
|
|
386
|
-
}));
|
|
387
|
-
// Only docx will trigger download
|
|
388
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(new ArrayBuffer(0)));
|
|
389
|
-
const result = await provider.fetchDocuments('site-001');
|
|
390
|
-
expect(result.isOk()).toBe(true);
|
|
391
|
-
if (result.isOk()) {
|
|
392
|
-
expect(result.value).toHaveLength(1);
|
|
393
|
-
expect(result.value[0].name).toBe('report.docx');
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
it('should skip items without file property (folders)', async () => {
|
|
397
|
-
await initializeProvider();
|
|
398
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
399
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
400
|
-
value: [
|
|
401
|
-
{
|
|
402
|
-
id: 'folder-001',
|
|
403
|
-
name: 'Reports',
|
|
404
|
-
size: 0,
|
|
405
|
-
webUrl: 'https://contoso.sharepoint.com/sites/eng/Documents/Reports',
|
|
406
|
-
lastModifiedDateTime: '2026-02-10T10:00:00Z',
|
|
407
|
-
// No file property — this is a folder
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
}));
|
|
411
|
-
const result = await provider.fetchDocuments('site-001');
|
|
412
|
-
expect(result.isOk()).toBe(true);
|
|
413
|
-
if (result.isOk()) {
|
|
414
|
-
expect(result.value).toEqual([]);
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
it('should handle PDF documents', async () => {
|
|
418
|
-
await initializeProvider();
|
|
419
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
420
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
421
|
-
value: [
|
|
422
|
-
createDriveItem({
|
|
423
|
-
id: 'pdf-001',
|
|
424
|
-
name: 'whitepaper.pdf',
|
|
425
|
-
file: { mimeType: 'application/pdf' },
|
|
426
|
-
}),
|
|
427
|
-
],
|
|
428
|
-
}));
|
|
429
|
-
// PDF download
|
|
430
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(new ArrayBuffer(0)));
|
|
431
|
-
const result = await provider.fetchDocuments('site-001');
|
|
432
|
-
expect(result.isOk()).toBe(true);
|
|
433
|
-
if (result.isOk()) {
|
|
434
|
-
expect(result.value).toHaveLength(1);
|
|
435
|
-
expect(result.value[0].mimeType).toBe('application/pdf');
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
it('should return err on Graph API failure for drives', async () => {
|
|
439
|
-
await initializeProvider();
|
|
440
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({}, 404, 'Not Found'));
|
|
441
|
-
const result = await provider.fetchDocuments('nonexistent-site');
|
|
442
|
-
expect(result.isErr()).toBe(true);
|
|
443
|
-
if (result.isErr()) {
|
|
444
|
-
expect(result.error.message).toContain('404');
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
it('should handle file download failure gracefully', async () => {
|
|
448
|
-
await initializeProvider();
|
|
449
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
450
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDriveItem()] }));
|
|
451
|
-
// Download fails
|
|
452
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
|
|
453
|
-
const result = await provider.fetchDocuments('site-001');
|
|
454
|
-
expect(result.isOk()).toBe(true);
|
|
455
|
-
if (result.isOk()) {
|
|
456
|
-
expect(result.value).toHaveLength(1);
|
|
457
|
-
expect(result.value[0].plainText).toBe('');
|
|
458
|
-
}
|
|
459
|
-
});
|
|
460
|
-
it('should return err on network failure', async () => {
|
|
461
|
-
await initializeProvider();
|
|
462
|
-
fetchSpy.mockRejectedValueOnce(new Error('Timeout'));
|
|
463
|
-
const result = await provider.fetchDocuments('site-001');
|
|
464
|
-
expect(result.isErr()).toBe(true);
|
|
465
|
-
if (result.isErr()) {
|
|
466
|
-
expect(result.error.message).toContain('Timeout');
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
it('should handle pagination for drive items', async () => {
|
|
470
|
-
await initializeProvider();
|
|
471
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
472
|
-
// First page
|
|
473
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
474
|
-
value: [createDriveItem({ id: 'doc-1', name: 'doc1.docx' })],
|
|
475
|
-
'@odata.nextLink': 'https://graph.microsoft.com/v1.0/sites/site-001/drives/drive-001/root/children?$skiptoken=xyz',
|
|
476
|
-
}));
|
|
477
|
-
// Download doc-1
|
|
478
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(new ArrayBuffer(0)));
|
|
479
|
-
// Second page
|
|
480
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
481
|
-
value: [createDriveItem({ id: 'doc-2', name: 'doc2.docx' })],
|
|
482
|
-
}));
|
|
483
|
-
// Download doc-2
|
|
484
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(new ArrayBuffer(0)));
|
|
485
|
-
const result = await provider.fetchDocuments('site-001');
|
|
486
|
-
expect(result.isOk()).toBe(true);
|
|
487
|
-
if (result.isOk()) {
|
|
488
|
-
expect(result.value).toHaveLength(2);
|
|
489
|
-
expect(result.value[0].id).toBe('doc-1');
|
|
490
|
-
expect(result.value[1].id).toBe('doc-2');
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
it('should use configured libraryNames when no explicit filter is given', async () => {
|
|
494
|
-
await initializeProvider();
|
|
495
|
-
// Returns two drives, only "Documents" matches config
|
|
496
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
497
|
-
value: [
|
|
498
|
-
createDrive({ id: 'drive-001', name: 'Documents' }),
|
|
499
|
-
createDrive({ id: 'drive-002', name: 'Archives' }),
|
|
500
|
-
],
|
|
501
|
-
}));
|
|
502
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
503
|
-
const result = await provider.fetchDocuments('site-001');
|
|
504
|
-
expect(result.isOk()).toBe(true);
|
|
505
|
-
// Only "Documents" drive should be queried
|
|
506
|
-
expect(fetchSpy).toHaveBeenCalledTimes(3); // token + drives + items
|
|
507
|
-
});
|
|
508
|
-
});
|
|
509
|
-
// --- getChangedItems ---
|
|
510
|
-
describe('getChangedItems', () => {
|
|
511
|
-
async function initializeProvider() {
|
|
512
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
513
|
-
await provider.initialize(VALID_CONFIG);
|
|
514
|
-
}
|
|
515
|
-
it('should fetch changed items since a date', async () => {
|
|
516
|
-
await initializeProvider();
|
|
517
|
-
// Drives for site-001
|
|
518
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
519
|
-
// Delta response
|
|
520
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
521
|
-
value: [
|
|
522
|
-
{
|
|
523
|
-
id: 'item-new',
|
|
524
|
-
name: 'new-doc.docx',
|
|
525
|
-
file: { mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
|
|
526
|
-
lastModifiedDateTime: '2026-02-20T12:00:00Z',
|
|
527
|
-
},
|
|
528
|
-
{
|
|
529
|
-
id: 'item-old',
|
|
530
|
-
name: 'old-doc.docx',
|
|
531
|
-
file: { mimeType: 'application/pdf' },
|
|
532
|
-
lastModifiedDateTime: '2026-01-01T00:00:00Z',
|
|
533
|
-
},
|
|
534
|
-
],
|
|
535
|
-
'@odata.deltaLink': 'https://graph.microsoft.com/v1.0/drives/drive-001/root/delta?token=xyz',
|
|
536
|
-
}));
|
|
537
|
-
// Drives for site-002
|
|
538
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive({ id: 'drive-002', name: 'Documents' })] }));
|
|
539
|
-
// Delta response for site-002
|
|
540
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
541
|
-
const since = new Date('2026-02-15T00:00:00Z');
|
|
542
|
-
const result = await provider.getChangedItems(since);
|
|
543
|
-
expect(result.isOk()).toBe(true);
|
|
544
|
-
if (result.isOk()) {
|
|
545
|
-
// Only the item modified after "since" should be included
|
|
546
|
-
expect(result.value).toHaveLength(1);
|
|
547
|
-
expect(result.value[0].id).toBe('item-new');
|
|
548
|
-
expect(result.value[0].type).toBe('document');
|
|
549
|
-
expect(result.value[0].changeType).toBe('updated');
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
it('should detect deleted items', async () => {
|
|
553
|
-
await initializeProvider();
|
|
554
|
-
// Drives for site-001
|
|
555
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
556
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
557
|
-
value: [
|
|
558
|
-
{
|
|
559
|
-
id: 'item-deleted',
|
|
560
|
-
name: 'removed.docx',
|
|
561
|
-
deleted: { state: 'deleted' },
|
|
562
|
-
lastModifiedDateTime: '2026-02-20T08:00:00Z',
|
|
563
|
-
},
|
|
564
|
-
],
|
|
565
|
-
}));
|
|
566
|
-
// Drives for site-002
|
|
567
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive({ id: 'drive-002', name: 'Documents' })] }));
|
|
568
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
569
|
-
const result = await provider.getChangedItems(new Date('2026-02-01'));
|
|
570
|
-
expect(result.isOk()).toBe(true);
|
|
571
|
-
if (result.isOk()) {
|
|
572
|
-
expect(result.value).toHaveLength(1);
|
|
573
|
-
expect(result.value[0].changeType).toBe('deleted');
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
it('should return err when no site IDs configured', async () => {
|
|
577
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse()));
|
|
578
|
-
await provider.initialize({
|
|
579
|
-
tenantId: 'abc',
|
|
580
|
-
clientId: 'def',
|
|
581
|
-
clientSecret: 'ghi',
|
|
582
|
-
});
|
|
583
|
-
const result = await provider.getChangedItems(new Date());
|
|
584
|
-
expect(result.isErr()).toBe(true);
|
|
585
|
-
if (result.isErr()) {
|
|
586
|
-
expect(result.error.message).toContain('No site IDs');
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
it('should return err on Graph API failure', async () => {
|
|
590
|
-
await initializeProvider();
|
|
591
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({}, 500, 'Internal Server Error'));
|
|
592
|
-
const result = await provider.getChangedItems(new Date());
|
|
593
|
-
expect(result.isErr()).toBe(true);
|
|
594
|
-
if (result.isErr()) {
|
|
595
|
-
expect(result.error.message).toContain('500');
|
|
596
|
-
}
|
|
597
|
-
});
|
|
598
|
-
it('should return empty array when no items changed', async () => {
|
|
599
|
-
await initializeProvider();
|
|
600
|
-
// Drives for site-001
|
|
601
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
602
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
603
|
-
// Drives for site-002
|
|
604
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive({ id: 'drive-002', name: 'Documents' })] }));
|
|
605
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
606
|
-
const result = await provider.getChangedItems(new Date());
|
|
607
|
-
expect(result.isOk()).toBe(true);
|
|
608
|
-
if (result.isOk()) {
|
|
609
|
-
expect(result.value).toEqual([]);
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
it('should handle delta pagination', async () => {
|
|
613
|
-
await initializeProvider();
|
|
614
|
-
// Drives for site-001
|
|
615
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive()] }));
|
|
616
|
-
// First delta page
|
|
617
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
618
|
-
value: [
|
|
619
|
-
{
|
|
620
|
-
id: 'change-1',
|
|
621
|
-
name: 'doc1.docx',
|
|
622
|
-
file: { mimeType: 'application/pdf' },
|
|
623
|
-
lastModifiedDateTime: '2026-02-20T10:00:00Z',
|
|
624
|
-
},
|
|
625
|
-
],
|
|
626
|
-
'@odata.nextLink': 'https://graph.microsoft.com/v1.0/drives/drive-001/root/delta?$skiptoken=abc',
|
|
627
|
-
}));
|
|
628
|
-
// Second delta page
|
|
629
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({
|
|
630
|
-
value: [
|
|
631
|
-
{
|
|
632
|
-
id: 'change-2',
|
|
633
|
-
name: 'doc2.pdf',
|
|
634
|
-
file: { mimeType: 'application/pdf' },
|
|
635
|
-
lastModifiedDateTime: '2026-02-21T10:00:00Z',
|
|
636
|
-
},
|
|
637
|
-
],
|
|
638
|
-
}));
|
|
639
|
-
// Drives for site-002
|
|
640
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [createDrive({ id: 'drive-002', name: 'Documents' })] }));
|
|
641
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
642
|
-
const result = await provider.getChangedItems(new Date('2026-02-01'));
|
|
643
|
-
expect(result.isOk()).toBe(true);
|
|
644
|
-
if (result.isOk()) {
|
|
645
|
-
expect(result.value).toHaveLength(2);
|
|
646
|
-
}
|
|
647
|
-
});
|
|
648
|
-
it('should return err on network error', async () => {
|
|
649
|
-
await initializeProvider();
|
|
650
|
-
fetchSpy.mockRejectedValueOnce(new Error('Connection refused'));
|
|
651
|
-
const result = await provider.getChangedItems(new Date());
|
|
652
|
-
expect(result.isErr()).toBe(true);
|
|
653
|
-
if (result.isErr()) {
|
|
654
|
-
expect(result.error.message).toContain('Connection refused');
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
});
|
|
658
|
-
// --- Error when not initialized ---
|
|
659
|
-
describe('when not initialized', () => {
|
|
660
|
-
it('should throw SharePointError from fetchPages', async () => {
|
|
661
|
-
await expect(provider.fetchPages()).rejects.toThrow(SharePointError);
|
|
662
|
-
});
|
|
663
|
-
it('should throw SharePointError from fetchDocuments', async () => {
|
|
664
|
-
await expect(provider.fetchDocuments('site-001')).rejects.toThrow(SharePointError);
|
|
665
|
-
});
|
|
666
|
-
it('should throw SharePointError from getChangedItems', async () => {
|
|
667
|
-
await expect(provider.getChangedItems(new Date())).rejects.toThrow(SharePointError);
|
|
668
|
-
});
|
|
669
|
-
});
|
|
670
|
-
// --- Token refresh ---
|
|
671
|
-
describe('token refresh', () => {
|
|
672
|
-
it('should re-acquire token when expired', async () => {
|
|
673
|
-
// Initial token with very short expiry (already expired)
|
|
674
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse(30)));
|
|
675
|
-
await provider.initialize(VALID_CONFIG);
|
|
676
|
-
// Token refresh
|
|
677
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse(createTokenResponse(3600)));
|
|
678
|
-
// Pages response
|
|
679
|
-
fetchSpy.mockResolvedValueOnce(createMockResponse({ value: [] }));
|
|
680
|
-
const result = await provider.fetchPages(['site-001']);
|
|
681
|
-
expect(result.isOk()).toBe(true);
|
|
682
|
-
// Should have made 3 calls: initial token + refresh token + pages
|
|
683
|
-
expect(fetchSpy).toHaveBeenCalledTimes(3);
|
|
684
|
-
});
|
|
685
|
-
});
|
|
686
|
-
});
|
|
687
|
-
// ---------------------------------------------------------------------------
|
|
688
|
-
// Text extraction
|
|
689
|
-
// ---------------------------------------------------------------------------
|
|
690
|
-
describe('extractTextFromDocx', () => {
|
|
691
|
-
it('should return empty string for empty buffer', () => {
|
|
692
|
-
expect(extractTextFromDocx(new ArrayBuffer(0))).toBe('');
|
|
693
|
-
});
|
|
694
|
-
it('should return empty string for non-ZIP data', () => {
|
|
695
|
-
const buffer = new TextEncoder().encode('not a zip file').buffer;
|
|
696
|
-
expect(extractTextFromDocx(buffer)).toBe('');
|
|
697
|
-
});
|
|
698
|
-
it('should extract text from a minimal docx-like ZIP structure', () => {
|
|
699
|
-
// Build a minimal ZIP containing word/document.xml with <w:t> elements
|
|
700
|
-
const xml = '<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>Hello World</w:t></w:r></w:p><w:p><w:r><w:t>Second paragraph</w:t></w:r></w:p></w:body></w:document>';
|
|
701
|
-
const buffer = buildMinimalZip('word/document.xml', xml);
|
|
702
|
-
const result = extractTextFromDocx(buffer);
|
|
703
|
-
expect(result).toContain('Hello World');
|
|
704
|
-
expect(result).toContain('Second paragraph');
|
|
705
|
-
});
|
|
706
|
-
it('should handle multiple w:t elements in a single paragraph', () => {
|
|
707
|
-
const xml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>Part 1 </w:t></w:r><w:r><w:t>Part 2</w:t></w:r></w:p></w:body></w:document>';
|
|
708
|
-
const buffer = buildMinimalZip('word/document.xml', xml);
|
|
709
|
-
const result = extractTextFromDocx(buffer);
|
|
710
|
-
expect(result).toContain('Part 1');
|
|
711
|
-
expect(result).toContain('Part 2');
|
|
712
|
-
});
|
|
713
|
-
it('should handle w:t with xml:space preserve attribute', () => {
|
|
714
|
-
const xml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t xml:space="preserve"> spaced text </w:t></w:r></w:p></w:body></w:document>';
|
|
715
|
-
const buffer = buildMinimalZip('word/document.xml', xml);
|
|
716
|
-
const result = extractTextFromDocx(buffer);
|
|
717
|
-
expect(result).toContain('spaced text');
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
describe('extractTextFromPdf', () => {
|
|
721
|
-
it('should return empty string for empty buffer', () => {
|
|
722
|
-
expect(extractTextFromPdf(new ArrayBuffer(0))).toBe('');
|
|
723
|
-
});
|
|
724
|
-
it('should extract text from Tj operators', () => {
|
|
725
|
-
const pdfContent = '%PDF-1.4\nBT\n/F1 12 Tf\n(Hello PDF World) Tj\nET\n';
|
|
726
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
727
|
-
const result = extractTextFromPdf(buffer);
|
|
728
|
-
expect(result).toContain('Hello PDF World');
|
|
729
|
-
});
|
|
730
|
-
it('should extract text from TJ array operators', () => {
|
|
731
|
-
const pdfContent = '%PDF-1.4\nBT\n/F1 12 Tf\n[(First) -10 (Second) -20 (Third)] TJ\nET\n';
|
|
732
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
733
|
-
const result = extractTextFromPdf(buffer);
|
|
734
|
-
expect(result).toContain('First');
|
|
735
|
-
expect(result).toContain('Second');
|
|
736
|
-
expect(result).toContain('Third');
|
|
737
|
-
});
|
|
738
|
-
it('should handle escaped characters in PDF strings', () => {
|
|
739
|
-
const pdfContent = '%PDF-1.4\nBT\n(Hello \\(world\\)) Tj\nET\n';
|
|
740
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
741
|
-
const result = extractTextFromPdf(buffer);
|
|
742
|
-
expect(result).toContain('Hello (world)');
|
|
743
|
-
});
|
|
744
|
-
it('should handle newline escapes in PDF strings', () => {
|
|
745
|
-
const pdfContent = '%PDF-1.4\nBT\n(Line1\\nLine2) Tj\nET\n';
|
|
746
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
747
|
-
const result = extractTextFromPdf(buffer);
|
|
748
|
-
expect(result).toContain('Line1');
|
|
749
|
-
expect(result).toContain('Line2');
|
|
750
|
-
});
|
|
751
|
-
it('should handle multiple BT/ET blocks', () => {
|
|
752
|
-
const pdfContent = '%PDF-1.4\nBT\n(Block 1) Tj\nET\nBT\n(Block 2) Tj\nET\n';
|
|
753
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
754
|
-
const result = extractTextFromPdf(buffer);
|
|
755
|
-
expect(result).toContain('Block 1');
|
|
756
|
-
expect(result).toContain('Block 2');
|
|
757
|
-
});
|
|
758
|
-
it('should return empty string for PDF without text content', () => {
|
|
759
|
-
const pdfContent = '%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n';
|
|
760
|
-
const buffer = new TextEncoder().encode(pdfContent).buffer;
|
|
761
|
-
const result = extractTextFromPdf(buffer);
|
|
762
|
-
expect(result).toBe('');
|
|
763
|
-
});
|
|
764
|
-
});
|
|
765
|
-
// ---------------------------------------------------------------------------
|
|
766
|
-
// ZIP builder helper (for docx tests)
|
|
767
|
-
// ---------------------------------------------------------------------------
|
|
768
|
-
/**
|
|
769
|
-
* Builds a minimal ZIP file containing a single uncompressed entry.
|
|
770
|
-
* This is a simplified ZIP structure for testing extractTextFromDocx.
|
|
771
|
-
*/
|
|
772
|
-
function buildMinimalZip(filename, content) {
|
|
773
|
-
const encoder = new TextEncoder();
|
|
774
|
-
const filenameBytes = encoder.encode(filename);
|
|
775
|
-
const contentBytes = encoder.encode(content);
|
|
776
|
-
const filenameLen = filenameBytes.length;
|
|
777
|
-
const contentLen = contentBytes.length;
|
|
778
|
-
// Local file header (30 + filenameLen bytes)
|
|
779
|
-
const localHeaderSize = 30 + filenameLen;
|
|
780
|
-
// Central directory header (46 + filenameLen bytes)
|
|
781
|
-
const centralHeaderSize = 46 + filenameLen;
|
|
782
|
-
// End of central directory (22 bytes)
|
|
783
|
-
const eocdSize = 22;
|
|
784
|
-
const totalSize = localHeaderSize + contentLen + centralHeaderSize + eocdSize;
|
|
785
|
-
const buffer = new ArrayBuffer(totalSize);
|
|
786
|
-
const view = new DataView(buffer);
|
|
787
|
-
const bytes = new Uint8Array(buffer);
|
|
788
|
-
let offset = 0;
|
|
789
|
-
// --- Local File Header ---
|
|
790
|
-
view.setUint32(offset, 0x04034b50, true); // signature
|
|
791
|
-
offset += 4;
|
|
792
|
-
view.setUint16(offset, 20, true); // version needed
|
|
793
|
-
offset += 2;
|
|
794
|
-
view.setUint16(offset, 0, true); // flags
|
|
795
|
-
offset += 2;
|
|
796
|
-
view.setUint16(offset, 0, true); // compression method (store)
|
|
797
|
-
offset += 2;
|
|
798
|
-
view.setUint16(offset, 0, true); // mod time
|
|
799
|
-
offset += 2;
|
|
800
|
-
view.setUint16(offset, 0, true); // mod date
|
|
801
|
-
offset += 2;
|
|
802
|
-
view.setUint32(offset, 0, true); // CRC32 (simplified)
|
|
803
|
-
offset += 4;
|
|
804
|
-
view.setUint32(offset, contentLen, true); // compressed size
|
|
805
|
-
offset += 4;
|
|
806
|
-
view.setUint32(offset, contentLen, true); // uncompressed size
|
|
807
|
-
offset += 4;
|
|
808
|
-
view.setUint16(offset, filenameLen, true); // filename length
|
|
809
|
-
offset += 2;
|
|
810
|
-
view.setUint16(offset, 0, true); // extra field length
|
|
811
|
-
offset += 2;
|
|
812
|
-
bytes.set(filenameBytes, offset);
|
|
813
|
-
offset += filenameLen;
|
|
814
|
-
bytes.set(contentBytes, offset);
|
|
815
|
-
offset += contentLen;
|
|
816
|
-
// --- Central Directory Header ---
|
|
817
|
-
const centralStart = offset;
|
|
818
|
-
view.setUint32(offset, 0x02014b50, true); // signature
|
|
819
|
-
offset += 4;
|
|
820
|
-
view.setUint16(offset, 20, true); // version made by
|
|
821
|
-
offset += 2;
|
|
822
|
-
view.setUint16(offset, 20, true); // version needed
|
|
823
|
-
offset += 2;
|
|
824
|
-
view.setUint16(offset, 0, true); // flags
|
|
825
|
-
offset += 2;
|
|
826
|
-
view.setUint16(offset, 0, true); // compression method
|
|
827
|
-
offset += 2;
|
|
828
|
-
view.setUint16(offset, 0, true); // mod time
|
|
829
|
-
offset += 2;
|
|
830
|
-
view.setUint16(offset, 0, true); // mod date
|
|
831
|
-
offset += 2;
|
|
832
|
-
view.setUint32(offset, 0, true); // CRC32
|
|
833
|
-
offset += 4;
|
|
834
|
-
view.setUint32(offset, contentLen, true); // compressed size
|
|
835
|
-
offset += 4;
|
|
836
|
-
view.setUint32(offset, contentLen, true); // uncompressed size
|
|
837
|
-
offset += 4;
|
|
838
|
-
view.setUint16(offset, filenameLen, true); // filename length
|
|
839
|
-
offset += 2;
|
|
840
|
-
view.setUint16(offset, 0, true); // extra field length
|
|
841
|
-
offset += 2;
|
|
842
|
-
view.setUint16(offset, 0, true); // file comment length
|
|
843
|
-
offset += 2;
|
|
844
|
-
view.setUint16(offset, 0, true); // disk number start
|
|
845
|
-
offset += 2;
|
|
846
|
-
view.setUint16(offset, 0, true); // internal attrs
|
|
847
|
-
offset += 2;
|
|
848
|
-
view.setUint32(offset, 0, true); // external attrs
|
|
849
|
-
offset += 4;
|
|
850
|
-
view.setUint32(offset, 0, true); // local header offset
|
|
851
|
-
offset += 4;
|
|
852
|
-
bytes.set(filenameBytes, offset);
|
|
853
|
-
offset += filenameLen;
|
|
854
|
-
// --- End of Central Directory ---
|
|
855
|
-
view.setUint32(offset, 0x06054b50, true); // signature
|
|
856
|
-
offset += 4;
|
|
857
|
-
view.setUint16(offset, 0, true); // disk number
|
|
858
|
-
offset += 2;
|
|
859
|
-
view.setUint16(offset, 0, true); // central dir disk
|
|
860
|
-
offset += 2;
|
|
861
|
-
view.setUint16(offset, 1, true); // entries on disk
|
|
862
|
-
offset += 2;
|
|
863
|
-
view.setUint16(offset, 1, true); // total entries
|
|
864
|
-
offset += 2;
|
|
865
|
-
view.setUint32(offset, centralHeaderSize, true); // central dir size
|
|
866
|
-
offset += 4;
|
|
867
|
-
view.setUint32(offset, centralStart, true); // central dir offset
|
|
868
|
-
offset += 4;
|
|
869
|
-
view.setUint16(offset, 0, true); // comment length
|
|
870
|
-
offset += 2;
|
|
871
|
-
return buffer;
|
|
872
|
-
}
|
|
873
|
-
//# sourceMappingURL=sharepoint-provider.test.js.map
|