|
@@ -75,7 +75,6 @@ const mockResponse = (
|
|
|
|
|
|
|
|
describe('NewsCronService', () => {
|
|
describe('NewsCronService', () => {
|
|
|
let service: NewsCronService;
|
|
let service: NewsCronService;
|
|
|
- const originalEnv = process.env.NEWS_FEED_URL;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
beforeEach(() => {
|
|
|
service = new NewsCronService();
|
|
service = new NewsCronService();
|
|
@@ -84,10 +83,6 @@ describe('NewsCronService', () => {
|
|
|
vi.spyOn(Math, 'random').mockReturnValue(0);
|
|
vi.spyOn(Math, 'random').mockReturnValue(0);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- afterEach(() => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = originalEnv;
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
describe('getCronSchedule', () => {
|
|
describe('getCronSchedule', () => {
|
|
|
test('should return daily schedule at midnight', () => {
|
|
test('should return daily schedule at midnight', () => {
|
|
|
expect(service.getCronSchedule()).toBe('0 0 * * *');
|
|
expect(service.getCronSchedule()).toBe('0 0 * * *');
|
|
@@ -96,7 +91,6 @@ describe('NewsCronService', () => {
|
|
|
|
|
|
|
|
describe('executeJob', () => {
|
|
describe('executeJob', () => {
|
|
|
test('should skip when news:isDeliveryEnabled is false', async () => {
|
|
test('should skip when news:isDeliveryEnabled is false', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.getConfig.mockImplementationOnce((key: string) =>
|
|
mocks.getConfig.mockImplementationOnce((key: string) =>
|
|
|
key === 'news:isDeliveryEnabled' ? false : undefined,
|
|
key === 'news:isDeliveryEnabled' ? false : undefined,
|
|
|
);
|
|
);
|
|
@@ -109,7 +103,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should run when news:isDeliveryEnabled is true (default)', async () => {
|
|
test('should run when news:isDeliveryEnabled is true (default)', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.mockFetch.mockResolvedValue(
|
|
mocks.mockFetch.mockResolvedValue(
|
|
|
mockResponse({ version: '1.0', items: [] }),
|
|
mockResponse({ version: '1.0', items: [] }),
|
|
|
);
|
|
);
|
|
@@ -120,63 +113,18 @@ describe('NewsCronService', () => {
|
|
|
expect(mocks.mockFetch).toHaveBeenCalled();
|
|
expect(mocks.mockFetch).toHaveBeenCalled();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- test('should skip when NEWS_FEED_URL is not set', async () => {
|
|
|
|
|
- delete process.env.NEWS_FEED_URL;
|
|
|
|
|
-
|
|
|
|
|
- await service.executeJob();
|
|
|
|
|
-
|
|
|
|
|
- expect(mocks.mockFetch).not.toHaveBeenCalled();
|
|
|
|
|
- expect(mocks.upsertNewsItems).not.toHaveBeenCalled();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- test('should skip when NEWS_FEED_URL is empty string', async () => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = '';
|
|
|
|
|
-
|
|
|
|
|
- await service.executeJob();
|
|
|
|
|
-
|
|
|
|
|
- expect(mocks.mockFetch).not.toHaveBeenCalled();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- test('should skip when NEWS_FEED_URL uses non-allowed http', async () => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = 'http://example.com/feed.json';
|
|
|
|
|
-
|
|
|
|
|
- await service.executeJob();
|
|
|
|
|
-
|
|
|
|
|
- expect(mocks.mockFetch).not.toHaveBeenCalled();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- test('should allow https:// URLs', async () => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
|
|
+ test('should fetch from the hardcoded vendor URL', async () => {
|
|
|
mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
|
|
|
|
|
|
await service.executeJob();
|
|
await service.executeJob();
|
|
|
|
|
|
|
|
expect(mocks.mockFetch).toHaveBeenCalledWith(
|
|
expect(mocks.mockFetch).toHaveBeenCalledWith(
|
|
|
- 'https://example.com/feed.json',
|
|
|
|
|
|
|
+ 'https://growilabs.github.io/growi-news-feed/feed.json',
|
|
|
expect.any(Object),
|
|
expect.any(Object),
|
|
|
);
|
|
);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- test('should allow http://localhost URLs', async () => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = 'http://localhost:8099/feed.json';
|
|
|
|
|
- mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
|
|
|
-
|
|
|
|
|
- await service.executeJob();
|
|
|
|
|
-
|
|
|
|
|
- expect(mocks.mockFetch).toHaveBeenCalled();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- test('should allow http://127.0.0.1 URLs', async () => {
|
|
|
|
|
- process.env.NEWS_FEED_URL = 'http://127.0.0.1:8099/feed.json';
|
|
|
|
|
- mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
|
|
|
-
|
|
|
|
|
- await service.executeJob();
|
|
|
|
|
-
|
|
|
|
|
- expect(mocks.mockFetch).toHaveBeenCalled();
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
test('should upsert items on successful fetch', async () => {
|
|
test('should upsert items on successful fetch', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
mocks.mockFetch.mockResolvedValue(mockResponse(VALID_FEED));
|
|
|
|
|
|
|
|
await service.executeJob();
|
|
await service.executeJob();
|
|
@@ -189,7 +137,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should NOT update DB when fetch fails', async () => {
|
|
test('should NOT update DB when fetch fails', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.mockFetch.mockResolvedValue({ ok: false, status: 500 });
|
|
mocks.mockFetch.mockResolvedValue({ ok: false, status: 500 });
|
|
|
|
|
|
|
|
await service.executeJob();
|
|
await service.executeJob();
|
|
@@ -199,7 +146,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should NOT update DB when fetch throws', async () => {
|
|
test('should NOT update DB when fetch throws', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.mockFetch.mockRejectedValue(new Error('Network error'));
|
|
mocks.mockFetch.mockRejectedValue(new Error('Network error'));
|
|
|
|
|
|
|
|
await expect(service.executeJob()).resolves.not.toThrow();
|
|
await expect(service.executeJob()).resolves.not.toThrow();
|
|
@@ -208,7 +154,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should filter items by growiVersionRegExps', async () => {
|
|
test('should filter items by growiVersionRegExps', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.getGrowiVersion.mockReturnValue('7.5.0');
|
|
mocks.getGrowiVersion.mockReturnValue('7.5.0');
|
|
|
const feedWithVersionFilter = {
|
|
const feedWithVersionFilter = {
|
|
|
version: '1.0',
|
|
version: '1.0',
|
|
@@ -237,7 +182,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should skip items with invalid growiVersionRegExps', async () => {
|
|
test('should skip items with invalid growiVersionRegExps', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.getGrowiVersion.mockReturnValue('7.5.0');
|
|
mocks.getGrowiVersion.mockReturnValue('7.5.0');
|
|
|
const feedWithInvalidRegex = {
|
|
const feedWithInvalidRegex = {
|
|
|
version: '1.0',
|
|
version: '1.0',
|
|
@@ -275,7 +219,6 @@ describe('NewsCronService', () => {
|
|
|
// cleaned up. The cron must now hand the full set of feed externalIds
|
|
// cleaned up. The cron must now hand the full set of feed externalIds
|
|
|
// to `deleteItemsNotInFeed`, which uses a $nin filter to remove the rest.
|
|
// to `deleteItemsNotInFeed`, which uses a $nin filter to remove the rest.
|
|
|
test('should pass every feed externalId to deleteItemsNotInFeed (regression for stale-item bug)', async () => {
|
|
test('should pass every feed externalId to deleteItemsNotInFeed (regression for stale-item bug)', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
const feed = {
|
|
const feed = {
|
|
|
version: '1.0',
|
|
version: '1.0',
|
|
|
items: [
|
|
items: [
|
|
@@ -315,7 +258,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should skip when response body exceeds size limit (5 MiB)', async () => {
|
|
test('should skip when response body exceeds size limit (5 MiB)', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
// Build a string that exceeds 5 MiB
|
|
// Build a string that exceeds 5 MiB
|
|
|
const oversizedText = 'x'.repeat(5 * 1024 * 1024 + 1);
|
|
const oversizedText = 'x'.repeat(5 * 1024 * 1024 + 1);
|
|
|
mocks.mockFetch.mockResolvedValue({
|
|
mocks.mockFetch.mockResolvedValue({
|
|
@@ -330,7 +272,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should abort when top-level shape is invalid', async () => {
|
|
test('should abort when top-level shape is invalid', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
// Missing `items` field — top-level schema check fails
|
|
// Missing `items` field — top-level schema check fails
|
|
|
mocks.mockFetch.mockResolvedValue(mockResponse({ version: '1.0' }));
|
|
mocks.mockFetch.mockResolvedValue(mockResponse({ version: '1.0' }));
|
|
|
|
|
|
|
@@ -341,7 +282,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should skip individual invalid items but keep valid ones', async () => {
|
|
test('should skip individual invalid items but keep valid ones', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
const feedWithMixedItems = {
|
|
const feedWithMixedItems = {
|
|
|
version: '1.0',
|
|
version: '1.0',
|
|
|
items: [
|
|
items: [
|
|
@@ -366,7 +306,6 @@ describe('NewsCronService', () => {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
test('should skip when response body is not valid JSON', async () => {
|
|
test('should skip when response body is not valid JSON', async () => {
|
|
|
- process.env.NEWS_FEED_URL = 'https://example.com/feed.json';
|
|
|
|
|
mocks.mockFetch.mockResolvedValue({
|
|
mocks.mockFetch.mockResolvedValue({
|
|
|
ok: true,
|
|
ok: true,
|
|
|
text: () => Promise.resolve('not-a-json{'),
|
|
text: () => Promise.resolve('not-a-json{'),
|