@dependabit/monitor 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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/checkers/github-repo.d.ts +17 -0
- package/dist/checkers/github-repo.d.ts.map +1 -0
- package/dist/checkers/github-repo.js +115 -0
- package/dist/checkers/github-repo.js.map +1 -0
- package/dist/checkers/index.d.ts +7 -0
- package/dist/checkers/index.d.ts.map +1 -0
- package/dist/checkers/index.js +7 -0
- package/dist/checkers/index.js.map +1 -0
- package/dist/checkers/openapi.d.ts +24 -0
- package/dist/checkers/openapi.d.ts.map +1 -0
- package/dist/checkers/openapi.js +221 -0
- package/dist/checkers/openapi.js.map +1 -0
- package/dist/checkers/url-content.d.ts +16 -0
- package/dist/checkers/url-content.d.ts.map +1 -0
- package/dist/checkers/url-content.js +66 -0
- package/dist/checkers/url-content.js.map +1 -0
- package/dist/comparator.d.ts +16 -0
- package/dist/comparator.d.ts.map +1 -0
- package/dist/comparator.js +53 -0
- package/dist/comparator.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/monitor.d.ts +43 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +85 -0
- package/dist/monitor.js.map +1 -0
- package/dist/normalizer.d.ts +24 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +97 -0
- package/dist/normalizer.js.map +1 -0
- package/dist/scheduler.d.ts +64 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +132 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/severity.d.ts +22 -0
- package/dist/severity.d.ts.map +1 -0
- package/dist/severity.js +87 -0
- package/dist/severity.js.map +1 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +39 -0
- package/src/checkers/github-repo.ts +150 -0
- package/src/checkers/index.ts +7 -0
- package/src/checkers/openapi.ts +310 -0
- package/src/checkers/url-content.ts +78 -0
- package/src/comparator.ts +68 -0
- package/src/index.ts +20 -0
- package/src/monitor.ts +120 -0
- package/src/normalizer.ts +122 -0
- package/src/scheduler.ts +175 -0
- package/src/severity.ts +112 -0
- package/src/types.ts +40 -0
- package/test/checkers/github-repo.test.ts +124 -0
- package/test/checkers/openapi.test.ts +352 -0
- package/test/checkers/url-content.test.ts +99 -0
- package/test/comparator.test.ts +108 -0
- package/test/monitor.test.ts +177 -0
- package/test/normalizer.test.ts +66 -0
- package/test/scheduler.test.ts +674 -0
- package/test/severity.test.ts +122 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common types for dependency checkers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface DependencySnapshot {
|
|
6
|
+
version?: string | undefined;
|
|
7
|
+
stateHash: string;
|
|
8
|
+
fetchedAt: Date;
|
|
9
|
+
metadata?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ChangeDetection {
|
|
13
|
+
hasChanged: boolean;
|
|
14
|
+
changes: string[];
|
|
15
|
+
oldVersion?: string | undefined;
|
|
16
|
+
newVersion?: string | undefined;
|
|
17
|
+
diff?: unknown;
|
|
18
|
+
severity?: 'breaking' | 'major' | 'minor';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AccessConfig {
|
|
22
|
+
url: string;
|
|
23
|
+
accessMethod: 'github-api' | 'http' | 'openapi' | 'context7' | 'arxiv';
|
|
24
|
+
auth?: {
|
|
25
|
+
type: 'token' | 'basic' | 'oauth' | 'none';
|
|
26
|
+
secret?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Checker {
|
|
31
|
+
/**
|
|
32
|
+
* Fetches the current state of a dependency
|
|
33
|
+
*/
|
|
34
|
+
fetch(config: AccessConfig): Promise<DependencySnapshot>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compares two snapshots to detect changes
|
|
38
|
+
*/
|
|
39
|
+
compare(prev: DependencySnapshot, curr: DependencySnapshot): Promise<ChangeDetection>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { GitHubRepoChecker } from '../../src/checkers/github-repo.js';
|
|
3
|
+
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
global.fetch = mockFetch as any;
|
|
7
|
+
|
|
8
|
+
describe('GitHubRepoChecker', () => {
|
|
9
|
+
let checker: GitHubRepoChecker;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
checker = new GitHubRepoChecker();
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
|
|
15
|
+
// Default mock response for releases
|
|
16
|
+
mockFetch.mockResolvedValue({
|
|
17
|
+
ok: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
json: async () => ({
|
|
20
|
+
tag_name: 'v1.0.0',
|
|
21
|
+
name: 'Release v1.0.0',
|
|
22
|
+
published_at: '2024-01-01T00:00:00Z',
|
|
23
|
+
body: 'Release notes',
|
|
24
|
+
html_url: 'https://github.com/owner/repo/releases/v1.0.0'
|
|
25
|
+
})
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('fetch', () => {
|
|
30
|
+
it('should fetch latest release information', async () => {
|
|
31
|
+
const config = {
|
|
32
|
+
url: 'https://github.com/owner/repo',
|
|
33
|
+
accessMethod: 'github-api' as const
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const result = await checker.fetch(config);
|
|
37
|
+
|
|
38
|
+
expect(result).toBeDefined();
|
|
39
|
+
expect(result.version).toBeDefined();
|
|
40
|
+
expect(result.stateHash).toBeDefined();
|
|
41
|
+
expect(result.fetchedAt).toBeInstanceOf(Date);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should handle repositories without releases', async () => {
|
|
45
|
+
// Mock 404 response for no releases, then mock commits response
|
|
46
|
+
mockFetch
|
|
47
|
+
.mockResolvedValueOnce({
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 404
|
|
50
|
+
})
|
|
51
|
+
.mockResolvedValueOnce({
|
|
52
|
+
ok: true,
|
|
53
|
+
status: 200,
|
|
54
|
+
json: async () => [
|
|
55
|
+
{
|
|
56
|
+
sha: 'abc123def456',
|
|
57
|
+
commit: {
|
|
58
|
+
message: 'Latest commit',
|
|
59
|
+
author: { date: '2024-01-01T00:00:00Z' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const config = {
|
|
66
|
+
url: 'https://github.com/owner/no-releases',
|
|
67
|
+
accessMethod: 'github-api' as const
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const result = await checker.fetch(config);
|
|
71
|
+
|
|
72
|
+
expect(result).toBeDefined();
|
|
73
|
+
expect(result.version).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should throw error for invalid GitHub URL', async () => {
|
|
77
|
+
const config = {
|
|
78
|
+
url: 'https://example.com/not-github',
|
|
79
|
+
accessMethod: 'github-api' as const
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await expect(checker.fetch(config)).rejects.toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('compare', () => {
|
|
87
|
+
it('should detect version changes', async () => {
|
|
88
|
+
const prev = {
|
|
89
|
+
version: 'v1.0.0',
|
|
90
|
+
stateHash: 'hash1',
|
|
91
|
+
fetchedAt: new Date('2024-01-01')
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const curr = {
|
|
95
|
+
version: 'v1.1.0',
|
|
96
|
+
stateHash: 'hash2',
|
|
97
|
+
fetchedAt: new Date('2024-01-02')
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = await checker.compare(prev, curr);
|
|
101
|
+
|
|
102
|
+
expect(result.hasChanged).toBe(true);
|
|
103
|
+
expect(result.changes).toContain('version');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return no change when versions match', async () => {
|
|
107
|
+
const prev = {
|
|
108
|
+
version: 'v1.0.0',
|
|
109
|
+
stateHash: 'hash1',
|
|
110
|
+
fetchedAt: new Date('2024-01-01')
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const curr = {
|
|
114
|
+
version: 'v1.0.0',
|
|
115
|
+
stateHash: 'hash1',
|
|
116
|
+
fetchedAt: new Date('2024-01-02')
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = await checker.compare(prev, curr);
|
|
120
|
+
|
|
121
|
+
expect(result.hasChanged).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { OpenAPIChecker } from '../../src/checkers/openapi.js';
|
|
3
|
+
import type { AccessConfig, DependencySnapshot } from '../../src/types.js';
|
|
4
|
+
|
|
5
|
+
describe('OpenAPIChecker', () => {
|
|
6
|
+
let checker: OpenAPIChecker;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
checker = new OpenAPIChecker();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('fetch', () => {
|
|
17
|
+
it('should fetch and parse JSON OpenAPI spec', async () => {
|
|
18
|
+
const mockSpec = {
|
|
19
|
+
openapi: '3.0.0',
|
|
20
|
+
info: {
|
|
21
|
+
title: 'Test API',
|
|
22
|
+
version: '1.0.0',
|
|
23
|
+
description: 'A test API'
|
|
24
|
+
},
|
|
25
|
+
paths: {
|
|
26
|
+
'/users': {
|
|
27
|
+
get: { operationId: 'getUsers' },
|
|
28
|
+
post: { operationId: 'createUser' }
|
|
29
|
+
},
|
|
30
|
+
'/users/{id}': {
|
|
31
|
+
get: { operationId: 'getUser' },
|
|
32
|
+
delete: { operationId: 'deleteUser' }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
components: {
|
|
36
|
+
schemas: {
|
|
37
|
+
User: { type: 'object' },
|
|
38
|
+
Error: { type: 'object' }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
vi.spyOn(global, 'fetch').mockResolvedValue({
|
|
44
|
+
ok: true,
|
|
45
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
46
|
+
text: async () => JSON.stringify(mockSpec)
|
|
47
|
+
} as Response);
|
|
48
|
+
|
|
49
|
+
const config: AccessConfig = {
|
|
50
|
+
url: 'https://api.example.com/openapi.json',
|
|
51
|
+
accessMethod: 'openapi'
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const snapshot = await checker.fetch(config);
|
|
55
|
+
|
|
56
|
+
expect(snapshot.version).toBe('1.0.0');
|
|
57
|
+
expect(snapshot.stateHash).toBeDefined();
|
|
58
|
+
expect(snapshot.fetchedAt).toBeInstanceOf(Date);
|
|
59
|
+
expect(snapshot.metadata?.title).toBe('Test API');
|
|
60
|
+
expect(snapshot.metadata?.endpointCount).toBe(2);
|
|
61
|
+
expect(snapshot.metadata?.schemaCount).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should fetch and parse YAML OpenAPI spec', async () => {
|
|
65
|
+
const yamlSpec = `
|
|
66
|
+
openapi: '3.0.0'
|
|
67
|
+
info:
|
|
68
|
+
title: Test API
|
|
69
|
+
version: '2.0.0'
|
|
70
|
+
paths:
|
|
71
|
+
/items:
|
|
72
|
+
get:
|
|
73
|
+
summary: List items
|
|
74
|
+
post:
|
|
75
|
+
summary: Create item
|
|
76
|
+
components:
|
|
77
|
+
schemas:
|
|
78
|
+
Item:
|
|
79
|
+
type: object
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
vi.spyOn(global, 'fetch').mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
headers: new Headers({ 'content-type': 'text/yaml' }),
|
|
85
|
+
text: async () => yamlSpec
|
|
86
|
+
} as Response);
|
|
87
|
+
|
|
88
|
+
const config: AccessConfig = {
|
|
89
|
+
url: 'https://api.example.com/openapi.yaml',
|
|
90
|
+
accessMethod: 'openapi'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const snapshot = await checker.fetch(config);
|
|
94
|
+
|
|
95
|
+
expect(snapshot.version).toBe('2.0.0');
|
|
96
|
+
expect(snapshot.metadata?.title).toBe('Test API');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle authentication token', async () => {
|
|
100
|
+
const mockSpec = { openapi: '3.0.0', info: { version: '1.0.0' } };
|
|
101
|
+
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue({
|
|
102
|
+
ok: true,
|
|
103
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
104
|
+
text: async () => JSON.stringify(mockSpec)
|
|
105
|
+
} as Response);
|
|
106
|
+
|
|
107
|
+
const config: AccessConfig = {
|
|
108
|
+
url: 'https://api.example.com/openapi.json',
|
|
109
|
+
accessMethod: 'openapi',
|
|
110
|
+
auth: {
|
|
111
|
+
type: 'token',
|
|
112
|
+
secret: 'test-token'
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await checker.fetch(config);
|
|
117
|
+
|
|
118
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
119
|
+
'https://api.example.com/openapi.json',
|
|
120
|
+
expect.objectContaining({
|
|
121
|
+
headers: expect.objectContaining({
|
|
122
|
+
Authorization: 'Bearer test-token'
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should throw error on HTTP failure', async () => {
|
|
129
|
+
vi.spyOn(global, 'fetch').mockResolvedValue({
|
|
130
|
+
ok: false,
|
|
131
|
+
status: 404,
|
|
132
|
+
statusText: 'Not Found'
|
|
133
|
+
} as Response);
|
|
134
|
+
|
|
135
|
+
const config: AccessConfig = {
|
|
136
|
+
url: 'https://api.example.com/openapi.json',
|
|
137
|
+
accessMethod: 'openapi'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
await expect(checker.fetch(config)).rejects.toThrow('HTTP error: 404 Not Found');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should throw error on invalid JSON', async () => {
|
|
144
|
+
vi.spyOn(global, 'fetch').mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
147
|
+
text: async () => 'invalid json'
|
|
148
|
+
} as Response);
|
|
149
|
+
|
|
150
|
+
const config: AccessConfig = {
|
|
151
|
+
url: 'https://api.example.com/openapi.json',
|
|
152
|
+
accessMethod: 'openapi'
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await expect(checker.fetch(config)).rejects.toThrow('Failed to fetch OpenAPI spec');
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('compare', () => {
|
|
160
|
+
it('should detect no changes when specs are identical', async () => {
|
|
161
|
+
const snapshot: DependencySnapshot = {
|
|
162
|
+
version: '1.0.0',
|
|
163
|
+
stateHash: 'abc123',
|
|
164
|
+
fetchedAt: new Date(),
|
|
165
|
+
metadata: {
|
|
166
|
+
endpoints: { '/users': ['GET', 'POST'] },
|
|
167
|
+
schemas: { User: {} }
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = await checker.compare(snapshot, { ...snapshot });
|
|
172
|
+
|
|
173
|
+
expect(result.hasChanged).toBe(false);
|
|
174
|
+
expect(result.changes).toHaveLength(0);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should detect added endpoints', async () => {
|
|
178
|
+
const prev: DependencySnapshot = {
|
|
179
|
+
version: '1.0.0',
|
|
180
|
+
stateHash: 'abc123',
|
|
181
|
+
fetchedAt: new Date(),
|
|
182
|
+
metadata: {
|
|
183
|
+
endpoints: { '/users': ['GET'] },
|
|
184
|
+
schemas: {}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const curr: DependencySnapshot = {
|
|
189
|
+
version: '1.0.0',
|
|
190
|
+
stateHash: 'def456',
|
|
191
|
+
fetchedAt: new Date(),
|
|
192
|
+
metadata: {
|
|
193
|
+
endpoints: { '/users': ['GET'], '/items': ['GET', 'POST'] },
|
|
194
|
+
schemas: {}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const result = await checker.compare(prev, curr);
|
|
199
|
+
|
|
200
|
+
expect(result.hasChanged).toBe(true);
|
|
201
|
+
expect(result.changes).toContain('endpoints_added');
|
|
202
|
+
expect(result.severity).toBe('minor');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should detect removed endpoints as breaking change', async () => {
|
|
206
|
+
const prev: DependencySnapshot = {
|
|
207
|
+
version: '1.0.0',
|
|
208
|
+
stateHash: 'abc123',
|
|
209
|
+
fetchedAt: new Date(),
|
|
210
|
+
metadata: {
|
|
211
|
+
endpoints: { '/users': ['GET'], '/items': ['GET'] },
|
|
212
|
+
schemas: {}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const curr: DependencySnapshot = {
|
|
217
|
+
version: '1.0.0',
|
|
218
|
+
stateHash: 'def456',
|
|
219
|
+
fetchedAt: new Date(),
|
|
220
|
+
metadata: {
|
|
221
|
+
endpoints: { '/users': ['GET'] },
|
|
222
|
+
schemas: {}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = await checker.compare(prev, curr);
|
|
227
|
+
|
|
228
|
+
expect(result.hasChanged).toBe(true);
|
|
229
|
+
expect(result.changes).toContain('endpoints_removed');
|
|
230
|
+
expect(result.severity).toBe('breaking');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should detect modified endpoints as major change', async () => {
|
|
234
|
+
const prev: DependencySnapshot = {
|
|
235
|
+
version: '1.0.0',
|
|
236
|
+
stateHash: 'abc123',
|
|
237
|
+
fetchedAt: new Date(),
|
|
238
|
+
metadata: {
|
|
239
|
+
endpoints: { '/users': ['GET', 'POST'] },
|
|
240
|
+
schemas: {}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const curr: DependencySnapshot = {
|
|
245
|
+
version: '1.0.0',
|
|
246
|
+
stateHash: 'def456',
|
|
247
|
+
fetchedAt: new Date(),
|
|
248
|
+
metadata: {
|
|
249
|
+
endpoints: { '/users': ['GET', 'POST', 'PUT'] },
|
|
250
|
+
schemas: {}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const result = await checker.compare(prev, curr);
|
|
255
|
+
|
|
256
|
+
expect(result.hasChanged).toBe(true);
|
|
257
|
+
expect(result.changes).toContain('endpoints_modified');
|
|
258
|
+
expect(result.severity).toBe('major');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should detect removed schemas as breaking change', async () => {
|
|
262
|
+
const prev: DependencySnapshot = {
|
|
263
|
+
version: '1.0.0',
|
|
264
|
+
stateHash: 'abc123',
|
|
265
|
+
fetchedAt: new Date(),
|
|
266
|
+
metadata: {
|
|
267
|
+
endpoints: {},
|
|
268
|
+
schemas: { User: {}, Item: {} }
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const curr: DependencySnapshot = {
|
|
273
|
+
version: '1.0.0',
|
|
274
|
+
stateHash: 'def456',
|
|
275
|
+
fetchedAt: new Date(),
|
|
276
|
+
metadata: {
|
|
277
|
+
endpoints: {},
|
|
278
|
+
schemas: { User: {} }
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = await checker.compare(prev, curr);
|
|
283
|
+
|
|
284
|
+
expect(result.hasChanged).toBe(true);
|
|
285
|
+
expect(result.changes).toContain('schemas_removed');
|
|
286
|
+
expect(result.severity).toBe('breaking');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should detect version changes', async () => {
|
|
290
|
+
const prev: DependencySnapshot = {
|
|
291
|
+
version: '1.0.0',
|
|
292
|
+
stateHash: 'abc123',
|
|
293
|
+
fetchedAt: new Date(),
|
|
294
|
+
metadata: {
|
|
295
|
+
endpoints: {},
|
|
296
|
+
schemas: {}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const curr: DependencySnapshot = {
|
|
301
|
+
version: '2.0.0',
|
|
302
|
+
stateHash: 'abc123',
|
|
303
|
+
fetchedAt: new Date(),
|
|
304
|
+
metadata: {
|
|
305
|
+
endpoints: {},
|
|
306
|
+
schemas: {}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const result = await checker.compare(prev, curr);
|
|
311
|
+
|
|
312
|
+
expect(result.hasChanged).toBe(true);
|
|
313
|
+
expect(result.changes).toContain('version');
|
|
314
|
+
expect(result.oldVersion).toBe('1.0.0');
|
|
315
|
+
expect(result.newVersion).toBe('2.0.0');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should include diff details', async () => {
|
|
319
|
+
const prev: DependencySnapshot = {
|
|
320
|
+
version: '1.0.0',
|
|
321
|
+
stateHash: 'abc123',
|
|
322
|
+
fetchedAt: new Date(),
|
|
323
|
+
metadata: {
|
|
324
|
+
endpoints: { '/users': ['GET'] },
|
|
325
|
+
schemas: { User: {} }
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const curr: DependencySnapshot = {
|
|
330
|
+
version: '2.0.0',
|
|
331
|
+
stateHash: 'def456',
|
|
332
|
+
fetchedAt: new Date(),
|
|
333
|
+
metadata: {
|
|
334
|
+
endpoints: { '/users': ['GET'], '/items': ['POST'] },
|
|
335
|
+
schemas: { User: {}, Item: {} }
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const result = await checker.compare(prev, curr);
|
|
340
|
+
|
|
341
|
+
expect(result.diff).toBeDefined();
|
|
342
|
+
const diff = result.diff as {
|
|
343
|
+
addedEndpoints: string[];
|
|
344
|
+
addedSchemas: string[];
|
|
345
|
+
versionChanged: boolean;
|
|
346
|
+
};
|
|
347
|
+
expect(diff.addedEndpoints).toContain('/items');
|
|
348
|
+
expect(diff.addedSchemas).toContain('Item');
|
|
349
|
+
expect(diff.versionChanged).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { URLContentChecker } from '../../src/checkers/url-content.js';
|
|
3
|
+
|
|
4
|
+
// Mock global fetch
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
global.fetch = mockFetch as any;
|
|
7
|
+
|
|
8
|
+
describe('URLContentChecker', () => {
|
|
9
|
+
let checker: URLContentChecker;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
checker = new URLContentChecker();
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
|
|
15
|
+
// Default mock response for HTTP content
|
|
16
|
+
mockFetch.mockResolvedValue({
|
|
17
|
+
ok: true,
|
|
18
|
+
status: 200,
|
|
19
|
+
headers: {
|
|
20
|
+
get: (name: string) => (name === 'content-type' ? 'text/html' : null)
|
|
21
|
+
},
|
|
22
|
+
text: async () => '<html><body>Test content</body></html>'
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('fetch', () => {
|
|
27
|
+
it('should fetch and hash URL content', async () => {
|
|
28
|
+
const config = {
|
|
29
|
+
url: 'https://example.com/docs',
|
|
30
|
+
accessMethod: 'http' as const
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = await checker.fetch(config);
|
|
34
|
+
|
|
35
|
+
expect(result).toBeDefined();
|
|
36
|
+
expect(result.stateHash).toBeDefined();
|
|
37
|
+
expect(result.stateHash).toMatch(/^[a-f0-9]{64}$/); // SHA256 hex
|
|
38
|
+
expect(result.fetchedAt).toBeInstanceOf(Date);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should normalize HTML before hashing', async () => {
|
|
42
|
+
const config = {
|
|
43
|
+
url: 'https://example.com/docs',
|
|
44
|
+
accessMethod: 'http' as const
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const result = await checker.fetch(config);
|
|
48
|
+
|
|
49
|
+
// Should produce consistent hash after normalization
|
|
50
|
+
expect(result.stateHash).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should throw error for unreachable URLs', async () => {
|
|
54
|
+
mockFetch.mockRejectedValue(new Error('Network error'));
|
|
55
|
+
|
|
56
|
+
const config = {
|
|
57
|
+
url: 'https://invalid-url-that-does-not-exist.com',
|
|
58
|
+
accessMethod: 'http' as const
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await expect(checker.fetch(config)).rejects.toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('compare', () => {
|
|
66
|
+
it('should detect content changes via hash difference', async () => {
|
|
67
|
+
const prev = {
|
|
68
|
+
stateHash: 'abc123def456',
|
|
69
|
+
fetchedAt: new Date('2024-01-01')
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const curr = {
|
|
73
|
+
stateHash: 'xyz789uvw012',
|
|
74
|
+
fetchedAt: new Date('2024-01-02')
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const result = await checker.compare(prev, curr);
|
|
78
|
+
|
|
79
|
+
expect(result.hasChanged).toBe(true);
|
|
80
|
+
expect(result.changes).toContain('content');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should return no change when hashes match', async () => {
|
|
84
|
+
const prev = {
|
|
85
|
+
stateHash: 'abc123def456',
|
|
86
|
+
fetchedAt: new Date('2024-01-01')
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const curr = {
|
|
90
|
+
stateHash: 'abc123def456',
|
|
91
|
+
fetchedAt: new Date('2024-01-02')
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = await checker.compare(prev, curr);
|
|
95
|
+
|
|
96
|
+
expect(result.hasChanged).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|