From d117b91c123a4ed0e9df817f311096ce2295bc93 Mon Sep 17 00:00:00 2001 From: Rotemh1 Date: Thu, 24 Jul 2025 18:05:42 +0300 Subject: [PATCH 1/2] field fetch tests --- backend/src/auth/auth.service.ts | 2 - .../field-fetch-api/convertXY.service.spec.ts | 55 ++++ .../src/field-fetch-api/convertXY.service.ts | 2 +- .../field-fetch-api.service.spec.ts | 305 ++++++++++++++++++ 4 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 backend/src/field-fetch-api/convertXY.service.spec.ts create mode 100644 backend/src/field-fetch-api/field-fetch-api.service.spec.ts diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 1b081fdd..d2f0ab2d 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -71,8 +71,6 @@ export class AuthService { role: user.role, }; } catch (error) { - console.error('Auth error:', error); - if (error instanceof UnauthorizedException) { throw error; } diff --git a/backend/src/field-fetch-api/convertXY.service.spec.ts b/backend/src/field-fetch-api/convertXY.service.spec.ts new file mode 100644 index 00000000..ce90681f --- /dev/null +++ b/backend/src/field-fetch-api/convertXY.service.spec.ts @@ -0,0 +1,55 @@ +import { convertXYService } from './convertXY.service'; +import { InternalServerErrorException } from '@nestjs/common'; + +describe('convertXYService', () => { + let service: convertXYService; + + beforeEach(() => { + service = new convertXYService(); + }); + + describe('convertXYtoLatLon', () => { + it('should return [lat, lon] for valid ITM coordinates', () => { + // Example ITM coordinates of Jerusalem + const x = 219529; + const y = 626907; + + const [lat, lon] = service.convertXYtoLatLon(x, y); + + // Check they are numbers and fall within reasonable range for Israel + expect(typeof lat).toBe('number'); + expect(typeof lon).toBe('number'); + expect(lat).toBeGreaterThan(29); // South of Israel ~29° + expect(lat).toBeLessThan(34); // North of Israel ~33.5° + expect(lon).toBeGreaterThan(33); // West ~33° + expect(lon).toBeLessThan(36); // East ~35.5° + }); + + it('should throw InternalServerErrorException if x or y is NaN', () => { + expect(() => service.convertXYtoLatLon(NaN, 123456)).toThrow( + InternalServerErrorException, + ); + expect(() => service.convertXYtoLatLon(123456, NaN)).toThrow( + InternalServerErrorException, + ); + }); + + it('should throw InternalServerErrorException if x or y is undefined', () => { + expect(() => service.convertXYtoLatLon(undefined as any, 123456)).toThrow( + InternalServerErrorException, + ); + expect(() => service.convertXYtoLatLon(123456, undefined as any)).toThrow( + InternalServerErrorException, + ); + }); + + it('should throw InternalServerErrorException if x or y is null', () => { + expect(() => service.convertXYtoLatLon(null as any, 123456)).toThrow( + InternalServerErrorException, + ); + expect(() => service.convertXYtoLatLon(123456, null as any)).toThrow( + InternalServerErrorException, + ); + }); + }); +}); diff --git a/backend/src/field-fetch-api/convertXY.service.ts b/backend/src/field-fetch-api/convertXY.service.ts index dae02204..9b9245ce 100644 --- a/backend/src/field-fetch-api/convertXY.service.ts +++ b/backend/src/field-fetch-api/convertXY.service.ts @@ -8,7 +8,7 @@ export class convertXYService { '+proj=tmerc +lat_0=31.73439361111111 +lon_0=35.20451694444445 +k=1.0000067 +x_0=219529.584 +y_0=626907.39 +ellps=GRS80 +units=m +no_defs'; convertXYtoLatLon(x: number, y: number): [number, number] { - if (!isNaN(x) && !isNaN(y)) { + if (!isNaN(x) && !isNaN(y) && x != null && y != null) { const [lon, lat] = proj4(this.ITM, this.WGS84, [x, y]); return [lat, lon]; } else throw new InternalServerErrorException(`${x}, ${y} not valid`); diff --git a/backend/src/field-fetch-api/field-fetch-api.service.spec.ts b/backend/src/field-fetch-api/field-fetch-api.service.spec.ts new file mode 100644 index 00000000..1908016b --- /dev/null +++ b/backend/src/field-fetch-api/field-fetch-api.service.spec.ts @@ -0,0 +1,305 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FieldFetchApiService } from './field-fetch-api.service'; +import { HttpService } from '@nestjs/axios'; +import { convertXYService } from './convertXY.service'; +import { FieldsService } from 'src/fields/fields.service'; +import { of } from 'rxjs'; +import { GameType } from 'src/enums/game-type.enum'; +import { AxiosResponse } from 'axios'; + +describe('FieldFetchApiService', () => { + let service: FieldFetchApiService; + let httpService: HttpService; + let fieldsService: FieldsService; + let xyService: convertXYService; + + const mockRecords = [ + { + _id: 1, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031G2308773', + 'סוג מתקן': "אולם ספורט קטן – 15x24 מ'", + 'שם המתקן': 'אולם התעמלות ביה"ס השלום', + 'שכונה-רובע': 'עבדאלרחמן', + רחוב: 'עבדאלרחמן - התמר', + 'מספר בית': '3', + 'ציר X': 210525, + 'ציר Y': 634529, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': + 'מועצה מקומית אבו גוש + ביה"ס יסודי השלום + מתנ"ס אבו גוש', + 'טלפון איש קשר': '025700342', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': null, + 'פנוי לפעילות': 'בתיאום בלבד', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'כן', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'תקין ופעיל', + 'מתקן תקני לתחרויות': 'תקני וכולל שירותים, מקלחות וחדר שופטים', + 'שימוש לתחרויות רשמיות': 'ליגה מקומית ואיזורית', + 'שנת הקמה': 2019, + 'משרת בית ספר': 'כן', + }, + { + _id: 2, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031G3301122', + 'סוג מתקן': "אולם ספורט בינוני – 32x19 מ'", + 'שם המתקן': 'אולם פיס אבו גוש', + 'שכונה-רובע': 'ליד תיכון אבו גוש', + רחוב: 'השלום', + 'מספר בית': '12', + 'ציר X': 210265, + 'ציר Y': 635399, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': 'רשות מקומית + מרכז קהילתי אבו גוש', + 'טלפון איש קשר': '02-5700342', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': 450, + 'פנוי לפעילות': 'מבוקר ועד לילה', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'כן', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'תקין ופעיל', + 'מתקן תקני לתחרויות': 'תקני וכולל שירותים, מקלחות וחדר שופטים', + 'שימוש לתחרויות רשמיות': 'מתקיימות במתקן תחרויות רשמיות', + 'שנת הקמה': 2008, + 'משרת בית ספר': 'חטיבת ביניים ותיכון אבו גוש', + }, + { + _id: 3, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031B1305253', + 'סוג מתקן': "מגרש כדורסל – 19X32 מ'", + 'שם המתקן': 'מגרש משולב כדורגל תיכון', + 'שכונה-רובע': 'ליד אולם ספורט', + רחוב: 'השלום', + 'מספר בית': '12', + 'ציר X': 210232, + 'ציר Y': 635383, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': 'רשות מקומית + מרכז קהילתי אבו גוש', + 'טלפון איש קשר': '02-5700342', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': 100, + 'פנוי לפעילות': 'פתוח ללא הגבלה', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'כן', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'פעיל וזקוק לשיפוץ', + 'מתקן תקני לתחרויות': 'המתקן אינו עומד במידות התקניות לתחרויות', + 'שימוש לתחרויות רשמיות': 'אינו משמש לתחרויות רשמיות', + 'שנת הקמה': 1994, + 'משרת בית ספר': 'חטיבת ביניים ותיכון אבו גוש', + }, + { + _id: 4, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031B1205740', + 'סוג מתקן': "מגרש כדורסל – 19X32 מ'", + 'שם המתקן': 'מגרש קטן חטיבת ביניים', + 'שכונה-רובע': '', + רחוב: 'השלום', + 'מספר בית': '', + 'ציר X': 210403, + 'ציר Y': 635436, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': 'רשות מקומית', + 'טלפון איש קשר': '02-5346975', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': 0, + 'פנוי לפעילות': 'בוקר בלבד', + 'גידור קיים': 'לא', + 'תאורה קיימת': 'לא', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'פעיל וזקוק לשיפוץ', + 'מתקן תקני לתחרויות': 'המתקן אינו עומד במידות התקניות לתחרויות', + 'שימוש לתחרויות רשמיות': 'אינו משמש לתחרויות רשמיות', + 'שנת הקמה': 2005, + 'משרת בית ספר': 'חטיבת ביניים', + }, + { + _id: 5, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031B2305241', + 'סוג מתקן': "מגרש ספורט משולב – 43X32 מ'", + 'שם המתקן': 'מגרש משולב יסודי מרכז', + 'שכונה-רובע': 'מרכז', + רחוב: 'מחמוד רשיד', + 'מספר בית': '1', + 'ציר X': 210393, + 'ציר Y': 635129, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': + 'רשות מקומית + מרכז קהילתי אבו גוש +ביה"ס יסודי אבו גוש מרכז', + 'טלפון איש קשר': '02-5342875', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': 150, + 'פנוי לפעילות': 'פתוח ללא הגבלה', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'כן', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'פעיל וזקוק לשיפוץ', + 'מתקן תקני לתחרויות': 'המתקן אינו עומד במידות התקניות לתחרויות', + 'שימוש לתחרויות רשמיות': 'אינו משמש לתחרויות רשמיות', + 'שנת הקמה': 1975, + 'משרת בית ספר': 'יסודי אבו גוש מרכז', + }, + { + _id: 6, + 'רשות מקומית': 'אבו גוש', + ישוב: '', + 'מספר זיהוי': '031B2205252', + 'סוג מתקן': "מגרש ספורט משולב – 43X32 מ'", + 'שם המתקן': 'מגרש משולב כדורגל יסודי השלום', + 'שכונה-רובע': 'עבדאלרחמן', + רחוב: 'עבדאלרחמן - התמר', + 'מספר בית': '1', + 'ציר X': 210523, + 'ציר Y': 634529, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו גוש', + 'גוף מפעיל המתקן': + 'מועצה מקומית אבו גוש + ביה"ס יסודי השלום + מתנ"ס אבו גוש', + 'טלפון איש קשר': '025700342', + 'דואל איש קשר': 'mohamad.jaber81@gmail.com', + 'מספר מושבים': 50, + 'פנוי לפעילות': 'מבוקר ועד לילה', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'לא', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'פעיל וזקוק לשיפוץ', + 'מתקן תקני לתחרויות': 'המתקן אינו עומד במידות התקניות לתחרויות', + 'שימוש לתחרויות רשמיות': 'לא רלוונטי', + 'שנת הקמה': 2010, + 'משרת בית ספר': 'יסודי השלום', + }, + { + _id: 7, + 'רשות מקומית': 'אבו סנאן', + ישוב: '', + 'מספר זיהוי': '032G2100771', + 'סוג מתקן': "אולם ספורט קטן – 15x24 מ'", + 'שם המתקן': 'אולם ספורט', + 'שכונה-רובע': 'שכונת החיילים', + רחוב: '', + 'מספר בית': '', + 'ציר X': 216156, + 'ציר Y': 762892, + 'מספר המבנים': null, + 'בעלי המתקן': 'אבו סנאן', + 'גוף מפעיל המתקן': 'מועצה מקומית אבו סנאן', + 'טלפון איש קשר': '049964527', + 'דואל איש קשר': 'tahseen.abreek@gmail.com', + 'מספר מושבים': null, + 'פנוי לפעילות': 'בתיאום בלבד', + 'גידור קיים': 'כן', + 'תאורה קיימת': 'כן', + 'נגישות לנכים': 'כן', + 'חניה לרכבים': 'כן', + 'מצב המתקן': 'לא פעיל', + 'מתקן תקני לתחרויות': 'המתקן אינו עומד במידות התקניות לתחרויות', + 'שימוש לתחרויות רשמיות': '', + 'שנת הקמה': 2018, + 'משרת בית ספר': '', + }, + ]; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FieldFetchApiService, + { + provide: HttpService, + useValue: { get: jest.fn() }, + }, + { + provide: convertXYService, + useValue: { + convertXYtoLatLon: jest + .fn() + .mockImplementation((x, y) => [x / 10000, y / 10000]), + }, + }, + { + provide: FieldsService, + useValue: { + upsertMany: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(FieldFetchApiService); + httpService = module.get(HttpService); + fieldsService = module.get(FieldsService); + xyService = module.get(convertXYService); + }); + + it('should parse and upsert fields correctly', async () => { + const axiosResponse: Partial> = { + data: { + result: { + records: mockRecords, + }, + }, + }; + + jest + .spyOn(httpService, 'get') + .mockReturnValue(of(axiosResponse as AxiosResponse)); + + const upsertSpy = jest.spyOn(fieldsService, 'upsertMany'); + + await service.getFields(); + + expect(httpService.get).toHaveBeenCalled(); + expect(xyService.convertXYtoLatLon).toHaveBeenCalled(); + + // Ensure only valid entries were passed to upsertMany + expect(upsertSpy).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + gameTypes: expect.arrayContaining([ + expect.stringMatching(/^(football|basketball|tennis)$/), + ]), + fieldLat: expect.any(Number), + fieldLng: expect.any(Number), + city: expect.any(String), + }), + ]), + ); + + // Ensure number of fields matches ones with matching opening hours and valid coordinates + const fields = upsertSpy.mock.calls[0][0]; + expect(fields.length).toBeGreaterThan(0); + + // Example field + expect(fields[0]).toEqual( + expect.objectContaining({ + fieldName: expect.any(String), + fieldLat: expect.any(Number), + fieldLng: expect.any(Number), + city: expect.any(String), + gameTypes: expect.any(Array), + }), + ); + }); +}); From 17ba42b01f66476afebef8ba4720ec7950c82e94 Mon Sep 17 00:00:00 2001 From: Rotemh1 Date: Sun, 27 Jul 2025 23:26:56 +0300 Subject: [PATCH 2/2] create group test --- backend/src/groups/groups.service.spec.ts | 103 ++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/backend/src/groups/groups.service.spec.ts b/backend/src/groups/groups.service.spec.ts index a9e562a3..0297da7c 100644 --- a/backend/src/groups/groups.service.spec.ts +++ b/backend/src/groups/groups.service.spec.ts @@ -9,6 +9,7 @@ import { UpdateGroupDto } from './dto/update-group.dto'; import { User } from 'src/users/users.entity'; import { Role } from 'src/enums/role.enum'; import { NotFoundException } from '@nestjs/common'; +import { GameType } from 'src/enums/game-type.enum'; //mock repository const mockGroupRepository = () => ({ @@ -79,6 +80,48 @@ describe('GroupsService', () => { }); }); + describe('createGroup', () => { + it('should create a group and add members', async () => { + const createGroupDto: CreateGroupDto = { + groupName: 'Test Group', + groupPicture: 'url', + gameTypes: [GameType.FootBall], + userIds: ['user1'], + }; + const mockGroup: Group = { + groupId: '1', + groupName: 'Test Group', + groupPicture: 'url', + gameTypes: [GameType.FootBall], + groupMembers: [], + } as Group; + + groupRepository.create.mockReturnValue(mockGroup); + groupRepository.save.mockResolvedValue(mockGroup); + groupRepository.findOne.mockResolvedValue(mockGroup); + + const result = await service.createGroup('creatorId', createGroupDto); + + expect(groupRepository.create).toHaveBeenCalledWith({ + groupName: 'Test Group', + groupPicture: 'url', + gameTypes: [GameType.FootBall], + }); + + expect(groupRepository.save).toHaveBeenCalled(); + expect(groupMembersService.addUsersToGroup).toHaveBeenCalledWith( + '1', + expect.arrayContaining(['creatorId', 'user1']), + ); + expect(groupMembersService.setManagerStatus).toHaveBeenCalledWith( + '1', + 'creatorId', + true, + ); + expect(result).toEqual(mockGroup); + }); + }); + describe('deleteGroup', () => { const groupId = 'group-123'; const adminUser = { uid: 'admin', role: Role.ADMIN } as User; @@ -129,4 +172,64 @@ describe('GroupsService', () => { expect(groupRepository.remove).not.toHaveBeenCalled(); }); }); + describe('updateGroup', () => { + const existingGroup = { + groupId: 'group-1', + groupName: 'Old Name', + groupPicture: 'old.png', + gameTypes: [GameType.BasketBall], + } as Group; + + beforeEach(() => { + groupRepository.save.mockClear(); + }); + + it('should update all fields and save group', async () => { + const dto: UpdateGroupDto = { + groupId: 'group-1', + groupName: 'New Name', + groupPicture: 'new.png', + gameTypes: [GameType.BasketBall], + }; + + groupRepository.findOne.mockResolvedValue(existingGroup); + groupRepository.save.mockResolvedValue({ + ...existingGroup, + ...dto, + }); + + const result = await service.updateGroup(dto); + + expect(result.groupName).toBe('New Name'); + expect(result.groupPicture).toBe('new.png'); + expect(result.gameTypes).toEqual([GameType.BasketBall]); + expect(groupRepository.save).toHaveBeenCalledWith({ + ...existingGroup, + ...dto, + }); + }); + + it('should update only specified fields', async () => { + const dto: UpdateGroupDto = { + groupId: 'group-1', + groupName: 'Partial Update', + gameTypes: [GameType.BasketBall, GameType.FootBall], + }; + + groupRepository.findOne.mockResolvedValue(existingGroup); + groupRepository.save.mockResolvedValue({ + ...existingGroup, + groupName: 'Partial Update', + gameTypes: [GameType.BasketBall, GameType.FootBall], + }); + + const result = await service.updateGroup(dto); + expect(result.groupName).toBe('Partial Update'); + expect(result.gameTypes).toEqual([ + GameType.BasketBall, + GameType.FootBall, + ]); + expect(groupRepository.save).toHaveBeenCalled(); + }); + }); });