From 862c94a4e634d5d400fd156c2c6bb344ab3fd715 Mon Sep 17 00:00:00 2001 From: albert Date: Mon, 28 Jul 2025 18:31:55 +0200 Subject: [PATCH] - update model and repository - add tests for feed Model --- src/__tests__/Feed.model.test.ts | 114 +++++++++++++++++++++++++++++++ src/models/Feed.ts | 99 +++++++++++++++++++++------ src/types/Feed.ts | 57 ++++++++++++++-- 3 files changed, 244 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/Feed.model.test.ts diff --git a/src/__tests__/Feed.model.test.ts b/src/__tests__/Feed.model.test.ts new file mode 100644 index 0000000..d4eefdd --- /dev/null +++ b/src/__tests__/Feed.model.test.ts @@ -0,0 +1,114 @@ +import mongoose from 'mongoose'; +import { Feed, IFeedDocument } from '../models/Feed.js'; +import { NewsSource } from '../types/Feed.js'; + +describe('Feed Model', () => { + const mockFeedData = { + title: 'Test News Title', + description: 'Test news description', + url: 'https://example.com/news/1', + source: NewsSource.EL_PAIS, + publishedAt: new Date('2024-01-15T10:00:00Z'), + imageUrl: 'https://example.com/image.jpg', + category: 'Politics', + isManual: false + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Schema Validation', () => { + test('should create a valid feed document', () => { + const feed = new Feed(mockFeedData); + + expect(feed.title).toBe(mockFeedData.title); + expect(feed.description).toBe(mockFeedData.description); + expect(feed.url).toBe(mockFeedData.url); + expect(feed.source).toBe(mockFeedData.source); + expect(feed.publishedAt).toEqual(mockFeedData.publishedAt); + expect(feed.imageUrl).toBe(mockFeedData.imageUrl); + expect(feed.category).toBe(mockFeedData.category); + expect(feed.isManual).toBe(mockFeedData.isManual); + }); + + test('should set default values correctly', () => { + const minimalData = { + title: 'Test Title', + description: 'Test description', + url: 'https://example.com/test', + source: NewsSource.EL_MUNDO, + publishedAt: new Date() + }; + + const feed = new Feed(minimalData); + + expect(feed.isManual).toBe(false); + expect(feed.createdAt).toBeDefined(); + expect(feed.updatedAt).toBeDefined(); + }); + + test('should require mandatory fields', () => { + const feed = new Feed({}); + const validationError = feed.validateSync(); + + expect(validationError?.errors.title).toBeDefined(); + expect(validationError?.errors.url).toBeDefined(); + expect(validationError?.errors.source).toBeDefined(); + expect(validationError?.errors.publishedAt).toBeDefined(); + }); + + test('should validate NewsSource enum', () => { + const invalidData = { + ...mockFeedData, + source: 'Invalid Source' as any + }; + + const feed = new Feed(invalidData); + const validationError = feed.validateSync(); + + expect(validationError?.errors.source).toBeDefined(); + }); + }); + + describe('Document Transformation', () => { + test('should transform _id to id in JSON', () => { + const feed = new Feed(mockFeedData); + feed._id = '507f1f77bcf86cd799439011'; + + const json = feed.toJSON(); + + expect(json.id.toString()).toBe('507f1f77bcf86cd799439011'); + expect(json._id).toBeUndefined(); + expect(json.__v).toBeUndefined(); + }); + + test('should transform _id to id in Object', () => { + const feed = new Feed(mockFeedData); + feed._id = '507f1f77bcf86cd799439011'; + + const obj = feed.toObject(); + + expect(obj.id.toString()).toBe('507f1f77bcf86cd799439011'); + expect(obj._id).toBeUndefined(); + expect(obj.__v).toBeUndefined(); + }); + }); + + describe('Model Methods', () => { + test('should create Feed model instance', () => { + expect(Feed).toBeDefined(); + expect(Feed.modelName).toBe('Feed'); + }); + + test('should have unique URL index', () => { + const indexes = Feed.schema.indexes(); + const urlIndex = indexes.find(index => + index[0].url && index[1].unique + ); + + expect(urlIndex).toBeDefined(); + expect(urlIndex?.[1].unique).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/src/models/Feed.ts b/src/models/Feed.ts index 5da0e23..19c7d4c 100644 --- a/src/models/Feed.ts +++ b/src/models/Feed.ts @@ -1,31 +1,86 @@ import mongoose, { Schema, Document } from 'mongoose'; -import { IFeed } from '../types/Feed.js'; +import { IFeed, NewsSource } from '../types/Feed.js'; export interface IFeedDocument extends IFeed, Document { - _id: string; + _id: string; } -const feedSchema = new Schema({ - }, { - timestamps: true, - toJSON: { - transform: function(doc, ret) { - ret.id = ret._id; - delete (ret as any)._id; - delete (ret as any).__v; - return ret; - } - }, - toObject: { - transform: function(doc, ret) { - ret.id = ret._id; - delete (ret as any)._id; - delete (ret as any).__v; - return ret; - } - } - }); +const feedSchemaObject = { + title: { + type: String, + required: true + }, + + description: { + type: String + }, + + url: { + type: String, + required: true, + unique: true + }, + + imageUrl: { + type: String + }, + + source: { + type: String, + required: true, + enum: Object.values(NewsSource) + }, + + category: { + type: String + }, + + publishedAt: { + type: Date, + required: true + }, + + createdAt: { + type: Date, + default: Date.now + }, + + updatedAt: { + type: Date, + default: Date.now + }, + + isManual: { + type: Boolean, + default: false + } +} as const; +const feedSchemaSettings = { + timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' }, + + toJSON: { + transform: (doc: any, ret: any) => { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + } + }, + + toObject: { + transform: (doc: any, ret: any) => { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + } + } +} as const; + +const feedSchema = new Schema(feedSchemaObject as any, feedSchemaSettings); + +feedSchema.index({ url: 1 }, { unique: true }); export const Feed = mongoose.model('Feed', feedSchema); export default Feed; \ No newline at end of file diff --git a/src/types/Feed.ts b/src/types/Feed.ts index 79db4c5..eee3741 100644 --- a/src/types/Feed.ts +++ b/src/types/Feed.ts @@ -2,9 +2,9 @@ export enum NewsSource { EL_PAIS = 'El PaĆ­s', EL_MUNDO = 'El Mundo', MANUAL = 'Manual' -} - -export interface IFeed { + } + + export interface IFeed { _id?: string; title: string; description: string; @@ -13,6 +13,55 @@ export interface IFeed { publishedAt: Date; imageUrl?: string; category?: string; + isManual: boolean; createdAt?: Date; updatedAt?: Date; -} + } + + export interface ICreateFeedDto { + title: string; + description: string; + url: string; + source: NewsSource; + publishedAt?: Date; + imageUrl?: string; + category?: string; + isManual?: boolean; + } + + export interface IUpdateFeedDto { + title?: string; + description?: string; + url?: string; + source?: NewsSource; + publishedAt?: Date; + imageUrl?: string; + category?: string; + isManual?: boolean; + } + + export interface IFeedQuery { + source?: NewsSource; + page?: number; + limit?: number; + } + + export interface IPaginatedResponse { + data: T[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; + }; + } + + export interface IScrapedNews { + title: string; + description: string; + url: string; + imageUrl?: string; + category?: string; + } \ No newline at end of file