From e09102eb6f0915a65991b92f4cc05df6e41a273b Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Tue, 20 Aug 2024 22:20:36 +0200 Subject: [PATCH 01/51] =?UTF-8?q?=F0=9F=94=B0=20refined=20tests=20for=20Au?= =?UTF-8?q?thController=20and=20appModule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Backend/package.json | 1 - Backend/src/app.module.spec.ts | 119 ++++++++++-------- .../auth/controller/auth.controller.spec.ts | 26 +++- 3 files changed, 93 insertions(+), 53 deletions(-) diff --git a/Backend/package.json b/Backend/package.json index e2c48164..120cbfc9 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -83,7 +83,6 @@ "coverageDirectory": "../coverage", "testEnvironment": "node", "coverageReporters": [ - "text-lcov", "text" ] } diff --git a/Backend/src/app.module.spec.ts b/Backend/src/app.module.spec.ts index 3e88728f..07152fab 100644 --- a/Backend/src/app.module.spec.ts +++ b/Backend/src/app.module.spec.ts @@ -1,52 +1,69 @@ -import { Test, TestingModule } from "@nestjs/testing"; -import { MongooseModule } from "@nestjs/mongoose"; -import { ConfigModule, ConfigService } from "@nestjs/config"; -import { Logger } from "@nestjs/common"; -//import { UserModule } from "./user/user.module"; -import { MongoMemoryServer } from "mongodb-memory-server"; - -describe("AppModule", () => { - let module: TestingModule; - let mongod: MongoMemoryServer; - - beforeAll(async () => { - mongod = new MongoMemoryServer(); - await mongod.start(); - }); - - afterAll(async () => { - await mongod.stop(); - }); - - beforeEach(async () => { - const uri = await mongod.getUri(); - const mockConfigService = { - get: jest.fn().mockImplementation((key: string) => { - if (key === "MONGODB_URI") { - return uri; - } - return null; - }), - }; - - module = await Test.createTestingModule({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - ], - }) - .overrideProvider(ConfigService) - .useValue(mockConfigService) - .compile(); - }); - - it("should compile the module", () => { - expect(module).toBeDefined(); - }); - - it("should use the correct MongoDB URI", () => { - const configService = module.get(ConfigService); - expect(configService.get("MONGODB_URI")).toBeDefined(); - }); +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from './app.module'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { AuthController } from './auth/controller/auth.controller'; +import { SpotifyController } from './spotify/controller/spotify.controller'; +import { YoutubeController } from './youtube/controller/youtube.controller'; +import { SearchController } from './search/controller/search.controller'; +import { AuthService } from './auth/services/auth.service'; +import { SupabaseService } from './supabase/services/supabase.service'; +import { ConfigService } from '@nestjs/config'; +import { SpotifyService } from './spotify/services/spotify.service'; +import { YoutubeService } from './youtube/services/youtube.service'; +import { SearchService } from './search/services/search.service'; +import { TokenMiddleware } from './middleware/token.middleware'; +import { MiddlewareConsumer, RequestMethod } from '@nestjs/common'; + +describe('AppModule', () => { + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + }); + + afterAll(async () => { + await module.close(); + }); + + it('should import ConfigModule and HttpModule', () => { + const imports = module.get(AppModule).constructor.prototype.constructor.parameters; + + expect(imports).toContainEqual(expect.arrayContaining([ConfigModule])); + expect(imports).toContainEqual(expect.arrayContaining([HttpModule])); + }); + + it('should have the correct controllers', () => { + const controllers = module.get(AppModule).constructor.prototype.controllers; + expect(controllers).toContainEqual(AuthController); + expect(controllers).toContainEqual(SpotifyController); + expect(controllers).toContainEqual(YoutubeController); + expect(controllers).toContainEqual(SearchController); + }); + + it('should have the correct providers', () => { + const providers = module.get(AppModule).constructor.prototype.providers; + expect(providers).toContainEqual(expect.anything()); + expect(providers).toContainEqual(AuthService); + expect(providers).toContainEqual(SupabaseService); + expect(providers).toContainEqual(ConfigService); + expect(providers).toContainEqual(SpotifyService); + expect(providers).toContainEqual(YoutubeService); + expect(providers).toContainEqual(SearchService); + }); + + it('should apply TokenMiddleware to auth/callback route', () => { + const consumer = { + apply: jest.fn().mockReturnThis(), + forRoutes: jest.fn().mockReturnValue({ path: 'auth/callback', method: RequestMethod.GET }), + } as unknown as MiddlewareConsumer; + + const appModule = new AppModule(); + appModule.configure(consumer); + + expect(consumer.apply).toHaveBeenCalledWith(TokenMiddleware); + expect(consumer.apply(TokenMiddleware).forRoutes).toHaveBeenCalledWith({ path: 'auth/callback', method: RequestMethod.GET }); + }); }); diff --git a/Backend/src/auth/controller/auth.controller.spec.ts b/Backend/src/auth/controller/auth.controller.spec.ts index 62e3e56f..37281f10 100644 --- a/Backend/src/auth/controller/auth.controller.spec.ts +++ b/Backend/src/auth/controller/auth.controller.spec.ts @@ -64,16 +64,40 @@ describe('AuthController', () => { tokens.providerRefreshToken ); }); + + it('should recieve an error', async () => { + const tokens = { + accessToken: '', + refreshToken: '', + providerToken: '', + providerRefreshToken: '', + }; + + const result = await controller.receiveTokens(tokens); + + expect(result).toEqual({ status: 'error', error: "Invalid tokens" }); + expect(supabaseService.handleSpotifyTokens).not.toHaveBeenCalled(); + }); }); describe('receiveCode', () => { it('should process received code', async () => { const code = 'authcode'; - await controller.receiveCode({ code }); + const result = await controller.receiveCode({ code }); + expect(result).toEqual({ message: "Code received and processed" }); expect(supabaseService.exchangeCodeForSession).toHaveBeenCalledWith(code); }); + + it('should return an error object', async () => { + const code = null; + + const result = await controller.receiveCode({ code }); + + expect(result).toEqual({status: 'error', error: "No code provided"}); + expect(supabaseService.exchangeCodeForSession).not.toHaveBeenCalled(); + }); }); describe('getProviderTokens', () => { From 778776adeeae3c834168e6f7c0c9db7bda1a99c6 Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Tue, 20 Aug 2024 23:24:20 +0200 Subject: [PATCH 02/51] =?UTF-8?q?=F0=9F=94=B0=20More=20tests=20for=20auth?= =?UTF-8?q?=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/auth.controller.spec.ts | 99 +++++++++++++++++-- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/Backend/src/auth/controller/auth.controller.spec.ts b/Backend/src/auth/controller/auth.controller.spec.ts index 37281f10..1dca931b 100644 --- a/Backend/src/auth/controller/auth.controller.spec.ts +++ b/Backend/src/auth/controller/auth.controller.spec.ts @@ -4,6 +4,7 @@ import { AuthService } from '../services/auth.service'; import { SupabaseService } from '../../supabase/services/supabase.service'; import { createSupabaseClient } from '../../supabase/services/supabaseClient'; import { Response } from 'express'; +import { HttpException, HttpStatus } from '@nestjs/common'; jest.mock('../../supabase/services/supabaseClient'); @@ -11,6 +12,9 @@ describe('AuthController', () => { let controller: AuthController; let authService: AuthService; let supabaseService: SupabaseService; + let mockSupabaseClient: any; + let mockResponse: Partial; + let consoleErrorSpy: jest.SpyInstance; beforeEach(async () => { const mockAuthService = { @@ -27,8 +31,18 @@ describe('AuthController', () => { exchangeCodeForSession: jest.fn(), retrieveTokens: jest.fn(), signInWithSpotifyOAuth: jest.fn(), + signinWithOAuth: jest.fn(), }; + mockSupabaseClient = { + auth: { + setSession: jest.fn(), + getUser: jest.fn(), + }, + }; + + (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabaseClient); + const module: TestingModule = await Test.createTestingModule({ controllers: [AuthController], providers: [ @@ -40,8 +54,20 @@ describe('AuthController', () => { controller = module.get(AuthController); authService = module.get(AuthService); supabaseService = module.get(SupabaseService); + + mockResponse = { + redirect: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn(), + }; + + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); }); + afterEach(() => { + consoleErrorSpy.mockRestore(); // Restore the original console.error after each test + }); + it('should be defined', () => { expect(controller).toBeDefined(); }); @@ -165,6 +191,34 @@ describe('AuthController', () => { message: 'Failed to retrieve provider tokens', }); }); + + it('should return an error when an acess token is not included', async () => { + const result = await controller.getProviderTokens({ + accessToken: null, + refreshToken: 'refresh', + }); + + expect(result).toEqual({ status: 'error', error: 'No access token or refresh token found in request.' }); + }); + + it('should throw a user error when one is returned by supabase.auth.getUser', async () =>{ + const mockUserResponse = { + data: null, + error: 'mockError' + }; + + mockSupabaseClient.auth.setSession.mockResolvedValue(undefined); + mockSupabaseClient.auth.getUser.mockResolvedValue(mockUserResponse); + + const result = await controller.getProviderTokens({ + accessToken: "access-token", + refreshToken: 'refresh-token', + }); + + expect(result).toEqual({ status: 'error', message: 'Failed to retrieve provider tokens' }); + expect(mockSupabaseClient.auth.setSession).toHaveBeenCalledWith('access-token', 'refresh-token'); + expect(mockSupabaseClient.auth.getUser).toHaveBeenCalledWith('access-token'); + }); }) describe('authCallback', () => { @@ -190,26 +244,53 @@ describe('AuthController', () => { expect(mockResponse.status).toHaveBeenCalledWith(400); expect(mockResponse.send).toHaveBeenCalledWith('Invalid token'); }); + + it('should print an error into the console and return an error 500', async () => { + const mockError = new Error('Session Error'); + (authService.setSession as jest.Mock).mockRejectedValue(mockError); + + await controller.authCallback('validAccessToken', 'validRefreshToken', mockResponse as Response); + + expect(authService.setSession).toHaveBeenCalledWith('validAccessToken', 'validRefreshToken'); + expect(mockResponse.status).toHaveBeenCalledWith(500); + expect(mockResponse.send).toHaveBeenCalledWith('Internal Server Error'); + expect(console.error).toHaveBeenCalledWith('Error setting session:', mockError); + + }); }); - /* + describe('signInWithSpotifyOAuth', () => { it('should redirect to Spotify OAuth URL', async () => { - const mockResponse = { - redirect: jest.fn(), - } as unknown as Response; + const mockUrl = 'http://oauth-url.com'; + (supabaseService.signinWithOAuth as jest.Mock).mockResolvedValue(mockUrl); + const result = await controller.signInWithSpotifyOAuth({provider: 'spotify'}); + + expect(supabaseService.signinWithOAuth).toHaveBeenCalledWith('spotify'); + expect(result).toEqual({url: mockUrl}); + }); - (supabaseService.signinWithOAuth('spotify') as unknown as jest.Mock).mockResolvedValue('https://spotify.oauth.url'); + it('should return an error if no provider is provided', async () => { + const result = await controller.signInWithSpotifyOAuth({provider: null}); - await controller.signInWithSpotifyOAuth(mockResponse); + expect(result).toEqual({ status: 'error', error: 'No provider specified' }); + expect(supabaseService.signinWithOAuth).not.toHaveBeenCalled(); + }); - expect(supabaseService.signinWithOAuth('spotify')).toHaveBeenCalled(); - expect(mockResponse.redirect).toHaveBeenCalledWith(303, 'https://spotify.oauth.url'); + it('should throw an HttpException if an error occurs during OAuth sign-in', async () => { + const mockError = new Error('OAuth sign-in failed'); + (supabaseService.signinWithOAuth as jest.Mock).mockRejectedValue(mockError); + + await expect(controller.signInWithSpotifyOAuth({ provider: 'spotify' })).rejects.toThrow( + new HttpException(mockError.message, HttpStatus.BAD_REQUEST) + ); + + expect(supabaseService.signinWithOAuth).toHaveBeenCalledWith('spotify'); }); }); -*/ + describe('signIn', () => { it('should sign in a user', async () => { const authDto = { email: 'test@example.com', password: 'password' }; From 63e6e1ff7f0b09dcb6383f66ba32a8c57d03e759 Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Wed, 21 Aug 2024 15:18:19 +0200 Subject: [PATCH 03/51] =?UTF-8?q?=F0=9F=94=B0=20100%=20coverage=20for=20au?= =?UTF-8?q?thcontroller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/auth.controller.spec.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Backend/src/auth/controller/auth.controller.spec.ts b/Backend/src/auth/controller/auth.controller.spec.ts index 1dca931b..5b635082 100644 --- a/Backend/src/auth/controller/auth.controller.spec.ts +++ b/Backend/src/auth/controller/auth.controller.spec.ts @@ -5,6 +5,8 @@ import { SupabaseService } from '../../supabase/services/supabase.service'; import { createSupabaseClient } from '../../supabase/services/supabaseClient'; import { Response } from 'express'; import { HttpException, HttpStatus } from '@nestjs/common'; +import exp from 'constants'; +import { ExternalExceptionFilterContext } from '@nestjs/core/exceptions/external-exception-filter-context'; jest.mock('../../supabase/services/supabaseClient'); @@ -303,6 +305,15 @@ describe('AuthController', () => { expect(authService.signIn).toHaveBeenCalledWith(authDto); expect(result).toEqual(mockResult); }); + + it('should return an error object', async () => { + const authDto = { email: '', password: 'password' }; + + const result = await controller.signIn(authDto); + + expect(result).toEqual({ error: "Invalid email or password" }); + expect(authService.signIn).not.toHaveBeenCalled(); + }); }); describe('signUp', () => { @@ -317,6 +328,15 @@ describe('AuthController', () => { expect(authService.signUp).toHaveBeenCalledWith(signUpData.email, signUpData.password, signUpData.metadata); expect(result).toEqual(mockResult); }); + + it('should return an error if email or password is missing', async () => { + const result = await controller.signUp({ email: '', password: '', metadata: {} }); + + expect(result).toEqual({ + status: 'error', + error: 'Invalid email or password', + }); + }); }); describe('signOut', () => { @@ -331,6 +351,15 @@ describe('AuthController', () => { expect(authService.signOut).toHaveBeenCalledWith(tokens.accessToken, tokens.refreshToken); expect(result).toEqual(mockResult); }); + + it('should return an error if accessToken or refreshToken is missing', async () => { + const result = await controller.signOut({ accessToken: '', refreshToken: '' }); + + expect(result).toEqual({ + status: 'error', + error: 'No access token or refresh token found in sign-out request.', + }); + }); }); describe('getCurrentUser', () => { @@ -345,6 +374,14 @@ describe('AuthController', () => { expect(authService.getCurrentUser).toHaveBeenCalledWith(tokens.accessToken, tokens.refreshToken); expect(result).toEqual({ user: mockUser }); }); + + it('should return error object', async () => { + const tokens = { accessToken: '', refreshToken: '' }; + + const result = await controller.getCurrentUser(tokens); + expect(result).toEqual({ status: 'error', error: "No access token or refresh token provided when attempting to retrieve current user." }); + expect(authService.getCurrentUser).not.toHaveBeenCalled(); + }); }); describe('getProvider', () => { @@ -359,5 +396,14 @@ describe('AuthController', () => { expect(authService.getProvider).toHaveBeenCalledWith(tokens.accessToken, tokens.refreshToken); expect(result).toEqual(mockResult); }); + + it('should return an error if accessToken or refreshToken is missing', async () => { + const result = await controller.getProvider({ accessToken: '', refreshToken: '' }); + + expect(result).toEqual({ + provider: "none", + message: "No access token or refresh token found in request." + }); + }); }); }); \ No newline at end of file From b63afabbcb30ce2403d21031cf6fb77fe0075061 Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Wed, 21 Aug 2024 16:56:29 +0200 Subject: [PATCH 04/51] =?UTF-8?q?=F0=9F=94=B0=20more=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/middleware/token.middleware.spec.ts | 67 ++++++++++ .../controller/search.controller.spec.ts | 126 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 Backend/src/middleware/token.middleware.spec.ts diff --git a/Backend/src/middleware/token.middleware.spec.ts b/Backend/src/middleware/token.middleware.spec.ts new file mode 100644 index 00000000..172677ee --- /dev/null +++ b/Backend/src/middleware/token.middleware.spec.ts @@ -0,0 +1,67 @@ +import { TokenMiddleware } from './token.middleware'; +import { Request, Response, NextFunction } from 'express'; + +describe('TokenMiddleware', () => { + let middleware: TokenMiddleware; + let mockRequest: Partial; + let mockResponse: Partial; + let mockNext: NextFunction; + + beforeEach(() => { + middleware = new TokenMiddleware(); + mockRequest = { + url: '', + query: {} + }; + mockResponse = {}; + mockNext = jest.fn(); + }); + + it('should extract access_token and refresh_token from the URL fragment and assign them to req.query', () => { + mockRequest.url = '/callback#access_token=abc123&refresh_token=def456'; + + middleware.use(mockRequest as Request, mockResponse as Response, mockNext); + + expect(mockRequest.query.access_token).toBe('abc123'); + expect(mockRequest.query.refresh_token).toBe('def456'); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should not assign anything to req.query if there is no URL fragment', () => { + mockRequest.url = '/callback'; + + middleware.use(mockRequest as Request, mockResponse as Response, mockNext); + + expect(mockRequest.query.access_token).toBeUndefined(); + expect(mockRequest.query.refresh_token).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should handle a fragment with no tokens correctly', () => { + mockRequest.url = '/callback#other_param=789'; + + middleware.use(mockRequest as Request, mockResponse as Response, mockNext); + + expect(mockRequest.query.access_token).toBeNull(); + expect(mockRequest.query.refresh_token).toBeNull(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should not modify req.query if there is no fragment in the URL', () => { + mockRequest.url = '/callback?param=123'; + + middleware.use(mockRequest as Request, mockResponse as Response, mockNext); + + expect(mockRequest.query.access_token).toBeUndefined(); + expect(mockRequest.query.refresh_token).toBeUndefined(); + expect(mockNext).toHaveBeenCalled(); + }); + + it('should call next even if no tokens are found', () => { + mockRequest.url = '/callback#'; + + middleware.use(mockRequest as Request, mockResponse as Response, mockNext); + + expect(mockNext).toHaveBeenCalled(); + }); +}); diff --git a/Backend/src/search/controller/search.controller.spec.ts b/Backend/src/search/controller/search.controller.spec.ts index b36477f2..33465d3c 100644 --- a/Backend/src/search/controller/search.controller.spec.ts +++ b/Backend/src/search/controller/search.controller.spec.ts @@ -1,6 +1,46 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SearchController } from './search.controller'; import { SearchService } from '../services/search.service'; +import { NotFoundException } from '@nestjs/common'; + +interface ArtistInfo +{ + name: string; + image: string; + topTracks: Track[]; + albums: Album[]; +} + +interface AlbumInfo extends Album +{ + releaseDate: string; + tracks: AlbumTrack[]; +} + +interface AlbumTrack +{ + id: number; + name: string; + duration: number; + trackNumber: number; + artistName: string; +} + +interface Track +{ + name: string; + albumName: string; + albumImageUrl: string; + artistName: string; +} + +interface Album +{ + id: number; + name: string; + imageUrl: string; + artistName: string; +} describe('SearchController', () => { let searchController: SearchController; @@ -15,6 +55,7 @@ describe('SearchController', () => { useValue: { searchByTitle: jest.fn(), searchByAlbum: jest.fn(), + artistSearch: jest.fn(), }, }, ], @@ -49,4 +90,89 @@ describe('SearchController', () => { expect(result).toEqual(searchResult); }); }); + + describe('searchForArtist', () => { + it('should call searchService.artistSearch with the correct artist name', async () => { + const artistName = 'Artist Name'; + const searchResult : ArtistInfo = { + name: 'Artist Name', + image: 'image', + topTracks: [ + { + name: 'name', + albumName: 'name', + albumImageUrl: 'url', + artistName: 'name', + } + ], + albums: [] + }; + + jest.spyOn(searchService, 'artistSearch').mockResolvedValue(searchResult); + + const result = await searchController.searchForArtist({ artist: artistName }); + + expect(searchService.artistSearch).toHaveBeenCalledWith(artistName); + expect(result).toEqual(searchResult); + }); + + it('should handle errors thrown by searchService.artistSearch', async () => { + const artistName = 'Artist Name'; + const error = new Error('Something went wrong'); + + jest.spyOn(searchService, 'artistSearch').mockRejectedValue(error); + + await expect(searchController.searchForArtist({ artist: artistName })).rejects.toThrow(error); + }); + }); + + describe('albumInfo', () => { + it('should return album info when the searchService returns data', async () => { + const title = 'Test Album'; + const albumData : AlbumInfo = { + id: 1, + name: "names", + imageUrl: 'coolpics', + artistName: 'coolname', + releaseDate: 'mock release date', + tracks: [ + { + id: 2, + name: "guh", + duration: 6, + trackNumber: 5, + artistName: 'guhguh' + }, + ]}; + + jest.spyOn(searchService, 'searchAlbums').mockResolvedValue(albumData); + + const result = await searchController.albumInfo({ title }); + + expect(searchService.searchAlbums).toHaveBeenCalledWith(title); + expect(result).toEqual(albumData); + }); + + it('should handle cases where no album data is found', async () => { + const title = 'Nonexistent Album'; + + jest.spyOn(searchService, 'searchAlbums').mockResolvedValue(null); + + const result = await searchController.albumInfo({ title }); + + expect(searchService.searchAlbums).toHaveBeenCalledWith(title); + expect(result).toBeNull(); + }); + + it('should throw an error if searchService.searchAlbums throws an exception', async () => { + const title = 'Test Album'; + + jest.spyOn(searchService, 'searchAlbums').mockRejectedValue(new NotFoundException()); + + await expect(searchController.albumInfo({ title })).rejects.toThrow(NotFoundException); + expect(searchService.searchAlbums).toHaveBeenCalledWith(title); + }); + + + }); }); From 07e1cd804d654df3e5faeffb8d62845952c8b1ff Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Mon, 2 Sep 2024 08:08:34 +0200 Subject: [PATCH 05/51] =?UTF-8?q?=F0=9F=94=B0=2069%=20coverage=20for=20spo?= =?UTF-8?q?tify=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/spotify.controller.spec.ts | 128 ++++++++++++++++-- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/Backend/src/spotify/controller/spotify.controller.spec.ts b/Backend/src/spotify/controller/spotify.controller.spec.ts index ca17523b..81209a12 100644 --- a/Backend/src/spotify/controller/spotify.controller.spec.ts +++ b/Backend/src/spotify/controller/spotify.controller.spec.ts @@ -9,13 +9,16 @@ describe('SpotifyController', () => { const mockSpotifyService = { getCurrentlyPlayingTrack: jest.fn(), - getRecentlyPlayedTracks: jest.fn(), - getQueue: jest.fn(), - playTrackById: jest.fn(), - pause: jest.fn(), - play: jest.fn(), - setVolume: jest.fn(), - getTrackDetails: jest.fn(), + getRecentlyPlayedTracks: jest.fn(), + getQueue: jest.fn(), + playTrackById: jest.fn(), + pause: jest.fn(), + play: jest.fn(), + setVolume: jest.fn(), + getTrackDetails: jest.fn(), + getTopArtists: jest.fn(), + getTrackAnalysis: jest.fn(), + getTopTracks: jest.fn(), }; beforeEach(async () => { @@ -149,6 +152,113 @@ describe('SpotifyController', () => { }); }); - - + describe('getTrackAnalysis', () => { + it('should get track analysis', async () => { + const body = { + trackId: 'mockID', + accessToken: 'mockToken', + refreshToken: 'mockToken' + }; + + mockSpotifyService.getTrackAnalysis.mockResolvedValue('response'); + + const result = await controller.getTrackAnalysis(body); + + expect(result).toEqual('response'); + expect(service.getTrackAnalysis).toHaveBeenCalled(); + }); + + it('should reject with accessToken excluded', async () => { + const body = { + trackId: 'mockID', + accessToken: '', + refreshToken: 'mockToken' + }; + + await expect(controller.getTrackAnalysis(body)).rejects.toThrow(new UnauthorizedException( + "TrackId, access token, or refresh token is missing while attempting to retrieve track analysis from Spotify." + )); + expect(service.getTrackAnalysis).not.toHaveBeenCalled(); + }); + + it('should reject with refreshToken excluded', async () => { + const body = { + trackId: 'mockID', + accessToken: 'mockToken', + refreshToken: '' + }; + + await expect(controller.getTrackAnalysis(body)).rejects.toThrow(new UnauthorizedException( + "TrackId, access token, or refresh token is missing while attempting to retrieve track analysis from Spotify." + )); + expect(service.getTrackAnalysis).not.toHaveBeenCalled(); + }); + }); + + describe('getTopTracks', () => { + it('should return top tracks', async () => { + const body = { + accessToken: 'token', + refreshToken: 'token' + } + + mockSpotifyService.getTopTracks.mockResolvedValue('result') + + await expect(controller.getTopTracks(body)).resolves.toEqual('result'); + expect(service.getTopTracks).toHaveBeenCalled(); + }); + + it('should reject with error when accesstoken excluded', async () => { + const body = { + accessToken: '', + refreshToken: 'token' + } + + mockSpotifyService.getTopTracks.mockResolvedValue('result') + + await expect(controller.getTopTracks(body)).rejects.toThrow(new UnauthorizedException("Access token, or refresh token is missing while attempting to retrieve track analysis from Spotify.")); + expect(service.getTopTracks).not.toHaveBeenCalled(); + }); + }); + + describe('getTopArtists', () => { + it('should return top artists', async () => { + const body = { + accessToken: 'mockToken', + refreshToken: 'mockToken' + }; + + mockSpotifyService.getTopArtists.mockResolvedValue('thing') + const result = await controller.getTopArtists(body); + + expect(result).toEqual('thing'); + expect(service.getTopArtists).toHaveBeenCalled(); + }); + + it('should return an error when access token is excluded', async () => { + const body = { + accessToken: '', + refreshToken: 'mocktoken' + }; + + await expect(controller.getTopArtists(body)).rejects.toThrow(new UnauthorizedException( + "Access token, or refresh token is missing while attempting to retrieve top artists from Spotify." + )); + + expect(service.getTopArtists).not.toHaveBeenCalled(); + }); + + it('should return an error when refresh token is excluded', async () => { + const body = { + accessToken: 'mocktoken', + refreshToken: '' + }; + + await expect(controller.getTopArtists(body)).rejects.toThrow(new UnauthorizedException( + "Access token, or refresh token is missing while attempting to retrieve top artists from Spotify." + )); + + expect(service.getTopArtists).not.toHaveBeenCalled(); + }); + }); }); From 30d4031127d17f555907483d52029a537efd165f Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Tue, 3 Sep 2024 06:11:54 +0200 Subject: [PATCH 06/51] =?UTF-8?q?=F0=9F=94=B0=20moar=20tests=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/spotify.controller.spec.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Backend/src/spotify/controller/spotify.controller.spec.ts b/Backend/src/spotify/controller/spotify.controller.spec.ts index 81209a12..78534161 100644 --- a/Backend/src/spotify/controller/spotify.controller.spec.ts +++ b/Backend/src/spotify/controller/spotify.controller.spec.ts @@ -19,6 +19,8 @@ describe('SpotifyController', () => { getTopArtists: jest.fn(), getTrackAnalysis: jest.fn(), getTopTracks: jest.fn(), + getTrackDetailsByName: jest.fn(), + addToQueue: jest.fn(), }; beforeEach(async () => { @@ -144,6 +146,62 @@ describe('SpotifyController', () => { }); }); + describe('addToQueue', () => { + it('should call addToQueue', async () => { + const body = { + uri: 'das', + device_id: 'fuggin', + accessToken: 'bo', + refreshToken: 'shitt' + }; + mockSpotifyService.addToQueue.mockReturnValue('dolphins'); + await controller.addToQueue(body); + expect(service.addToQueue).toHaveBeenCalledWith(body.uri, body.device_id, body.accessToken, body.refreshToken); + }); + + it('should throw an error', async () => { + const body = { + uri: '', + device_id: 'fuggin', + accessToken: 'bo', + refreshToken: 'shitt' + }; + await expect(controller.addToQueue(body)).resolves.toEqual( + { + status: "error", + error: "Artist, song name, access token or refresh token is missing while attempting to retrieve suggested songs from the ECHO API." + } + ) + expect(service.addToQueue).not.toHaveBeenCalled(); + }); + }); + + describe('getTrackDetailsByName', () => { + it('should call getTrackDetailsByName method of SpotifyService with correct parameters', async () => { + const body = { + artistName: 'ThatcherJoe', + trackName: 'JoeSugg', + accessToken: 'testAccessToken', + refreshToken: 'testRefreshToken' + }; + mockSpotifyService.getTrackDetailsByName.mockReturnValue('dolphins'); + await controller.getTrackDetailsByName(body); + expect(service.getTrackDetailsByName).toHaveBeenCalledWith(body.artistName, body.trackName, body.accessToken, body.refreshToken); + }); + + it('should throw an error', async () => { + const body = { + artistName: '', + trackName: 'JoeSugg', + accessToken: 'testAccessToken', + refreshToken: 'testRefreshToken' + }; + await expect(controller.getTrackDetailsByName(body)).rejects.toThrow(new UnauthorizedException( + "Artist name, track name, access token, or refresh token is missing while attempting to retrieve track details from Spotify.")); + expect(service.getTrackDetailsByName).not.toHaveBeenCalled(); + }); + }); + describe('playTrackByName', () => { it('should call playTrackByName method of SpotifyService with correct parameters', async () => { const body = { trackName: 'testTrackName', accessToken: 'testAccessToken', refreshToken: 'testRefreshToken' }; From 0e0d46f1664f5e3e344f04988465aaa519831d6a Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Tue, 3 Sep 2024 06:48:36 +0200 Subject: [PATCH 07/51] =?UTF-8?q?=F0=9F=94=B0=20Spotify=20Controller=20100?= =?UTF-8?q?%=20line=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/spotify.controller.spec.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/Backend/src/spotify/controller/spotify.controller.spec.ts b/Backend/src/spotify/controller/spotify.controller.spec.ts index 78534161..d67ce83f 100644 --- a/Backend/src/spotify/controller/spotify.controller.spec.ts +++ b/Backend/src/spotify/controller/spotify.controller.spec.ts @@ -21,6 +21,10 @@ describe('SpotifyController', () => { getTopTracks: jest.fn(), getTrackDetailsByName: jest.fn(), addToQueue: jest.fn(), + seekToPosition: jest.fn(), + getTrackDuration: jest.fn(), + playPreviousTrack: jest.fn(), + playNextTrack: jest.fn(), }; beforeEach(async () => { @@ -104,6 +108,14 @@ describe('SpotifyController', () => { await controller.getQueue(body); expect(service.getQueue).toHaveBeenCalledWith(body.artist, body.song_name, body.accessToken, body.refreshToken); }); + it('should return an error', async () => { + const body = { artist: '', song_name: 'testSong', accessToken: 'testAccessToken', refreshToken: 'testRefreshToken' }; + await expect(controller.getQueue(body)).resolves.toEqual({ + status: "error", + error: "Artist, song name, access token or refresh token is missing while attempting to retrieve suggested songs from the ECHO API." + }); + expect(service.getQueue).not.toHaveBeenCalled(); + }); }); describe('playTrackById', () => { @@ -120,6 +132,11 @@ describe('SpotifyController', () => { await controller.pause(body); expect(service.pause).toHaveBeenCalledWith(body.accessToken, body.refreshToken); }); + it('should reject and throw an error', async () => { + const body = { accessToken: '', refreshToken: 'testRefreshToken' }; + await expect(controller.pause(body)).rejects.toThrow(new UnauthorizedException("Access token or refresh token is missing while attempting to pause the currently playing song from Spotify.")); + expect(service.pause).not.toHaveBeenCalled(); + }); }); describe('play', () => { @@ -128,6 +145,12 @@ describe('SpotifyController', () => { await controller.play(body); expect(service.play).toHaveBeenCalledWith(body.accessToken, body.refreshToken); }); + + it('should reject and throw an error', async () => { + const body = { accessToken: '', refreshToken: 'testRefreshToken' }; + await expect(controller.play(body)).rejects.toThrow(new UnauthorizedException("Access token or refresh token is missing while attempting to resume the currently paused song from Spotify.")); + expect(service.play).not.toHaveBeenCalled(); + }); }); describe('setVolume', () => { @@ -136,6 +159,12 @@ describe('SpotifyController', () => { await controller.setVolume(body); expect(service.setVolume).toHaveBeenCalledWith(body.volume, body.accessToken, body.refreshToken); }); + + it('should reject and throw an error', async () => { + const body = { volume: null, accessToken: 'testAccessToken', refreshToken: 'testRefreshToken' }; + await expect(controller.setVolume(body)).rejects.toThrow(new UnauthorizedException("Volume, access token, or refresh token is missing while attempting to set the volume of a device from Spotify.")); + expect(service.setVolume).not.toHaveBeenCalledWith(body.volume, body.accessToken, body.refreshToken); + }); }); describe('getTrackDetails', () => { @@ -144,6 +173,83 @@ describe('SpotifyController', () => { await controller.getTrackDetails(body); expect(service.getTrackDetails).toHaveBeenCalledWith(body.trackID, body.accessToken, body.refreshToken); }); + + it('should reject and throw an error', async () => { + const body = { trackID: '', accessToken: 'testAccessToken', refreshToken: 'testRefreshToken' }; + await expect(controller.getTrackDetails(body)).rejects.toThrow(new UnauthorizedException("Track ID, access token, or refresh token is missing while attempting to retrieve the details of a track from Spotify.")); + expect(service.getTrackDetails).not.toHaveBeenCalledWith(body.trackID, body.accessToken, body.refreshToken); + }); + }); + + describe('playNextTrack', () => { + it('should call playNextTrack', async () => { + const body = { + accessToken: 'string', refreshToken: 'string', deviceId: 'string' + }; + mockSpotifyService.playNextTrack.mockReturnValue('dolphins'); + await controller.playNextTrack(body); + expect(service.playNextTrack).toHaveBeenCalled(); + }); + + it('should throw an error', async () => { + const body = { + accessToken: '', refreshToken: 'string', deviceId: 'string' + }; + await expect(controller.playNextTrack(body)).rejects.toThrow( + new UnauthorizedException("Access token, refresh token, or device ID is missing while attempting to play the next song from Spotify.") + ) + expect(service.playNextTrack).not.toHaveBeenCalled(); + }); + }); + + describe('playPreviousTrack', () => { + it('should call playPreviousTrack', async () => { + const body = { + accessToken: 'string', refreshToken: 'string', deviceId: 'string' + }; + mockSpotifyService.playPreviousTrack.mockReturnValue('dolphins'); + await controller.playPreviousTrack(body); + expect(service.playPreviousTrack).toHaveBeenCalled(); + }); + + it('should throw an error', async () => { + const body = { + accessToken: '', refreshToken: 'string', deviceId: 'string' + }; + await expect(controller.playPreviousTrack(body)).rejects.toThrow( + new UnauthorizedException("Access token, refresh token, or device ID is missing while attempting to play the next song from Spotify.") + ) + expect(service.playPreviousTrack).not.toHaveBeenCalled(); + }); + }); + + //technically an integration test since we are using multiple functions in one call + describe('seekToPosition', () => { + mockSpotifyService.seekToPosition.mockReturnValue('seekPos'); + mockSpotifyService.getTrackDuration.mockReturnValue(20); + it('should call seekToPosition Successfully', async () => { + const body = { + deviceId: 'string', + progress: 6, + accessToken: 'string', + refreshToken: 'string' + }; + await expect(controller.seekToPosition(body)).resolves.toEqual('seekPos'); + expect(service.seekToPosition).toHaveBeenCalled(); + }); + + it('should throw an error', async () => { + const body = { + deviceId: '', + progress: 6, + accessToken: 'string', + refreshToken: 'string' + }; + mockSpotifyService.seekToPosition.mockReturnValue('seekPos'); + + await expect(controller.seekToPosition(body)).rejects.toThrow(new UnauthorizedException("Access token, refresh token, or device ID is missing while attempting to seek to a position with Spotify.")); + expect(service.seekToPosition).not.toHaveBeenCalled(); + }); }); describe('addToQueue', () => { From 1205de59f3c75fefb558eb7fabd0891faedf4473 Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Fri, 6 Sep 2024 10:12:13 +0200 Subject: [PATCH 08/51] =?UTF-8?q?=F0=9F=94=B0=20Spotify=20Service=2083%=20?= =?UTF-8?q?coverage.=20Will=20not=20test=20further=20due=20to=20presence?= =?UTF-8?q?=20of=20protected=20members.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit all private members in spotify.service.ts changed to protected for testability purposes. Added new dependency: @types/axios Coverage now at 72.41 for Backend. --- Backend/package-lock.json | 16 +- Backend/package.json | 1 + .../spotify/services/spotify.service.spec.ts | 1132 +++++++++++++++-- .../src/spotify/services/spotify.service.ts | 4 +- 4 files changed, 1057 insertions(+), 96 deletions(-) diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 1f1158e4..d9f9a1dd 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -30,6 +30,7 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", "@nestjs/testing": "^10.3.9", + "@types/axios": "^0.14.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", @@ -2125,6 +2126,16 @@ "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true }, + "node_modules/@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==", + "deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!", + "dev": true, + "dependencies": { + "axios": "*" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3073,7 +3084,6 @@ "version": "1.7.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3084,7 +3094,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8072,8 +8081,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "peer": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { "version": "1.9.0", diff --git a/Backend/package.json b/Backend/package.json index 120cbfc9..757f5445 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -43,6 +43,7 @@ "@nestjs/cli": "^10.3.2", "@nestjs/schematics": "^10.1.1", "@nestjs/testing": "^10.3.9", + "@types/axios": "^0.14.0", "@types/bcryptjs": "^2.4.6", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", diff --git a/Backend/src/spotify/services/spotify.service.spec.ts b/Backend/src/spotify/services/spotify.service.spec.ts index 16e904de..24caed3b 100644 --- a/Backend/src/spotify/services/spotify.service.spec.ts +++ b/Backend/src/spotify/services/spotify.service.spec.ts @@ -1,9 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { HttpService, HttpModule } from '@nestjs/axios'; -import { of } from 'rxjs'; +import { lastValueFrom, firstValueFrom, of, throwError } from 'rxjs'; import { SupabaseService } from '../../supabase/services/supabase.service'; import { SpotifyService } from './spotify.service'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { AxiosResponse } from 'axios'; jest.mock('../../supabase/services/supabaseClient', () => ({ createSupabaseClient: jest.fn(() => ({ @@ -17,12 +18,48 @@ jest.mock('../../supabase/services/supabaseClient', () => ({ })), })); +jest.mock('rxjs', () => ({ + ...jest.requireActual('rxjs'), + lastValueFrom: jest.fn(), + firstValueFrom: jest.fn(), + of: jest.fn(), +})); + +class TestableSpotifyService extends SpotifyService { + public async getAccessToken(accessToken: string, refreshToken: string): Promise { + return super.getAccessToken(accessToken, refreshToken); + } + + public async getAccessKey(): Promise { + return super.getAccessKey(); + } + + public async fetchSpotifyTracks(trackIds: string, accessToken: string, refreshToken: string): Promise { + return super.fetchSpotifyTracks(trackIds, accessToken, refreshToken); + } +} + +const mockTracksResponse = { + data: { + items: [ + { + id: '1', + name: 'Track 1', + album: { name: 'Album 1', images: [{ url: 'http://album1.jpg' }] }, + artists: [{ name: 'Artist 1' }], + preview_url: 'http://preview1.mp3', + external_urls: { spotify: 'http://spotify1.com' }, + }, + ], + }, +}; + jest.mock('../../config', () => ({ accessKey: 'test-access-key', })); describe('SpotifyService', () => { - let service: SpotifyService; + let service: TestableSpotifyService; let httpService: HttpService; let supabaseService: SupabaseService; @@ -30,7 +67,10 @@ describe('SpotifyService', () => { const module: TestingModule = await Test.createTestingModule({ imports: [HttpModule], providers: [ - SpotifyService, + { + provide: SpotifyService, + useClass: TestableSpotifyService, // Override with the subclass + }, { provide: SupabaseService, useValue: { @@ -40,7 +80,7 @@ describe('SpotifyService', () => { ], }).compile(); - service = module.get(SpotifyService); + service = module.get(SpotifyService); httpService = module.get(HttpService); supabaseService = module.get(SupabaseService); }); @@ -75,31 +115,180 @@ describe('SpotifyService', () => { expect(result).toEqual({ items: ['track1', 'track2'] }); }); }); -/* - describe('getQueue', () => { - it('should return recommended tracks', async () => { - const mockResponse = { data: { recommended_tracks: [{ track_uri: 'spotify:track:1' }, { track_uri: 'spotify:track:2' }] } }; - const mockFetchTracks = jest.spyOn(service as any, 'fetchSpotifyTracks').mockResolvedValue(['track1', 'track2']); - jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse) as any); - const result = await service.getQueue('artist', 'song_name', 'accessToken', 'refreshToken'); - expect(result).toEqual(['track1', 'track2']); - expect(mockFetchTracks).toHaveBeenCalledWith('1,2', 'accessToken', 'refreshToken'); // Updated to match the received value + describe('getQueue', () => { + it('should successfully get queue from recommendations', async () => { + // Arrange + const artist = 'mockArtist'; + const songName = 'mockSong'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockAccessKey = 'mockAccessKey'; + const mockResponse = { + data: { + recommended_songs: [ + { track: { track_uri: 'spotify:track:123456' } }, + { track: 'spotify:track:789012' } + ] + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + } as AxiosResponse; + const mockTrackDetails = [ + { id: '123456', name: 'Mock Track 1', albumName: 'Mock Album 1' }, + { id: '789012', name: 'Mock Track 2', albumName: 'Mock Album 2' } + ]; + + // Mock the getAccessKey method to return a fake access key + jest.spyOn(service, 'getAccessKey').mockResolvedValue(mockAccessKey); + + // Mock the HTTP request to return a successful response + jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse)); + + // Mock the fetchSpotifyTracks method to return track details + jest.spyOn(service, 'fetchSpotifyTracks').mockResolvedValue(mockTrackDetails); + + // Act + const result = await service.getQueue(artist, songName, accessToken, refreshToken); + + // Assert + expect(service.getAccessKey).toHaveBeenCalled(); + expect(httpService.post).toHaveBeenCalledWith( + "https://echo-ai-interface.azurewebsites.net/api/get_recommendations", + { + access_key: mockAccessKey, + artist: artist, + song_name: songName + }, + { + headers: { "Content-Type": "application/json" } + } + ); + expect(service.fetchSpotifyTracks).toHaveBeenCalledWith('123456,789012', accessToken, refreshToken); + expect(result).toEqual(mockTrackDetails); + }); + + it('should handle errors when fetching queue', async () => { + // Arrange + const artist = 'mockArtist'; + const songName = 'mockSong'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockAccessKey = 'mockAccessKey'; + + // Mock the getAccessKey method to return a fake access key + jest.spyOn(service, 'getAccessKey').mockResolvedValue(mockAccessKey); + + // Mock the HTTP request to throw an error + jest.spyOn(httpService, 'post').mockReturnValue(throwError(() => new Error('Network Error'))); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.getQueue(artist, songName, accessToken, refreshToken)).rejects.toThrow('Network Error'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching queue:", new Error('Network Error')); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); -*/ -/* - describe('playTrackById', () => { - it('should play a track by ID', async () => { - const mockResponse = { data: 'mockPlayResponse' }; - jest.spyOn(httpService, 'put').mockReturnValue(of(mockResponse) as any); - const result = await service.playTrackById('trackId', 'deviceId', 'accessToken', 'refreshToken'); - expect(result).toEqual({"_finalizers": null, "_parentage": null, "closed": true, "destination": null, "initialTeardown": undefined, "isStopped": true}); + + describe('playTrackById', () => { + it('should successfully play a track by ID', async () => { + // Arrange + const trackId = 'mockTrackId'; + const deviceId = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + const mockResponse = { data: 'Track played successfully' } as AxiosResponse; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return a successful response + jest.spyOn(httpService, 'put').mockReturnValue(of(mockResponse)); + + // Act + const result = await service.playTrackById(trackId, deviceId, accessToken, refreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(httpService.put).toHaveBeenCalledWith( + `https://api.spotify.com/v1/me/player/play?device_id=${deviceId}`, + { uris: [`spotify:track:${trackId}`] }, + { + headers: { + "Authorization": `Bearer ${mockProviderToken}`, + "Content-Type": "application/json" + } + } + ); + expect(result).toEqual(mockResponse.data); + }); + + it('should handle errors when playing a track', async () => { + // Arrange + const trackId = 'mockTrackId'; + const deviceId = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to throw an error + jest.spyOn(httpService, 'put').mockReturnValue(throwError(() => new Error('Network Error'))); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.playTrackById(trackId, deviceId, accessToken, refreshToken)).rejects.toThrow('Network Error'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error playing track by ID:", new Error('Network Error')); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + + it('should handle invalid responses when playing a track', async () => { + // Arrange + const trackId = 'mockTrackId'; + const deviceId = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return an invalid response + jest.spyOn(httpService, 'put').mockReturnValue(of({ data: null } as AxiosResponse)); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.playTrackById(trackId, deviceId, accessToken, refreshToken)).rejects.toThrow('Error playing track by ID'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error playing track by ID:", new Error('Error playing track by ID')); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); -*/ + describe('pause', () => { it('should pause the currently playing track', async () => { const mockResponse = { data: 'mockPauseResponse' }; @@ -121,114 +310,877 @@ describe('SpotifyService', () => { }); describe('setVolume', () => { - it('should set the player volume', async () => { - const mockResponse = { data: 'mockVolumeResponse' }; - jest.spyOn(httpService, 'put').mockReturnValue(of(mockResponse) as any); - - const result = await service.setVolume(50, 'accessToken', 'refreshToken'); - expect(result).toEqual({"_finalizers": null, "_parentage": null, "closed": true, "destination": null, "initialTeardown": undefined, "isStopped": true}); + it('should successfully set the volume', async () => { + // Arrange + const volume = 50; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + const mockResponse = { data: 'Volume set successfully' } as AxiosResponse; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return a successful response + jest.spyOn(httpService, 'put').mockReturnValue(of(mockResponse)); + + // Act + const result = await service.setVolume(volume, accessToken, refreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(httpService.put).toHaveBeenCalledWith( + `https://api.spotify.com/v1/me/player/volume?volume_percent=${volume}`, + {}, + { headers: { "Authorization": `Bearer ${mockProviderToken}` } } + ); + expect(result).toEqual(mockResponse.data); + }); + + it('should handle errors when setting the volume', async () => { + // Arrange + const volume = 50; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to throw an error + jest.spyOn(httpService, 'put').mockReturnValue(throwError(() => new Error('Network Error'))); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.setVolume(volume, accessToken, refreshToken)).rejects.toThrow('Network Error'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error setting volume:", new Error('Network Error')); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + + it('should throw an error if the response is invalid', async () => { + // Arrange + const volume = 50; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return an invalid response + jest.spyOn(httpService, 'put').mockReturnValue(of({ data: null } as AxiosResponse) ); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.setVolume(volume, accessToken, refreshToken)).rejects.toThrow('Error setting volume'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error setting volume:", new Error('Error setting volume')); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); describe('getTrackDetails', () => { - it('should return track details', async () => { - const mockResponse = { data: { id: 'track1' } }; - jest.spyOn(httpService, 'get').mockReturnValue(of(mockResponse) as any); - - const result = await service.getTrackDetails('track1', 'accessToken', 'refreshToken'); - expect(result).toEqual({ id: 'track1' }); + it('should successfully retrieve track details', async () => { + // Arrange + const trackID = 'mockTrackID'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + const mockTrackData = { id: trackID, name: 'Mock Track', album: { name: 'Mock Album' } }; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return mock track data + jest.spyOn(httpService, 'get').mockReturnValue({ + pipe: jest.fn().mockReturnThis(), + } as any); + + (lastValueFrom as jest.Mock).mockResolvedValue({ data: mockTrackData }); + + // Act + const result = await service.getTrackDetails(trackID, accessToken, refreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(httpService.get).toHaveBeenCalledWith(`https://api.spotify.com/v1/tracks/${trackID}`, { + headers: { "Authorization": `Bearer ${mockProviderToken}` } + }); + expect(result).toEqual(mockTrackData); + }); + + it('should throw an error if there is an issue with the API call', async () => { + // Arrange + const trackID = 'mockTrackID'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to throw an error + jest.spyOn(httpService, 'get').mockImplementation(() => { + throw new Error('Network Error'); + }); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.getTrackDetails(trackID, accessToken, refreshToken)).rejects.toThrow('Network Error'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching track details:", new Error('Network Error')); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + + it('should throw an error if the response is invalid', async () => { + // Arrange + const trackID = 'mockTrackID'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the HTTP request to return an invalid response + jest.spyOn(httpService, 'get').mockReturnValue({ + pipe: jest.fn().mockReturnThis(), + } as any); + + (lastValueFrom as jest.Mock).mockResolvedValue({ data: null }); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.getTrackDetails(trackID, accessToken, refreshToken)).rejects.toThrow('Error fetching track details'); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching track details:", new Error('Error fetching track details')); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); describe('playNextTrack', () => { - it('should play the next track', async () => { - const mockResponse = { status: 204 }; - jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse) as any); - - const result = await service.playNextTrack('accessToken', 'refreshToken', 'deviceId'); - expect(result).toEqual({ message: 'Skipped to next track successfully' }); + it('should successfully skip to the next track', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API for successful track switch + (firstValueFrom as jest.Mock).mockResolvedValue({ status: 204 }); + + // Act + const result = await service.playNextTrack(accessToken, refreshToken, deviceId); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(firstValueFrom).toHaveBeenCalledTimes(1); + expect(result).toEqual({ message: "Skipped to next track successfully" }); + }); + + it('should throw HttpException if there is an error skipping to the next track', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock firstValueFrom to throw an error + (firstValueFrom as jest.Mock).mockRejectedValue(new Error('Network Error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.playNextTrack(accessToken, refreshToken, deviceId)).rejects.toThrow( + new HttpException("Failed to play next track", HttpStatus.INTERNAL_SERVER_ERROR), + ); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error playing next track:', 'Network Error'); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); describe('playPreviousTrack', () => { - it('should play the previous track', async () => { - const mockResponse = { status: 204 }; - jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse) as any); - - const result = await service.playPreviousTrack('accessToken', 'refreshToken', 'deviceId'); - expect(result).toEqual({ message: 'Switched to previous track successfully' }); + it('should successfully switch to the previous track', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API for successful track switch + (firstValueFrom as jest.Mock).mockResolvedValue({ status: 204 }); + + // Act + const result = await service.playPreviousTrack(accessToken, refreshToken, deviceId); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(firstValueFrom).toHaveBeenCalledTimes(1); + expect(result).toEqual({ message: "Switched to previous track successfully" }); + }); + + it('should throw HttpException if there is an error switching tracks', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock firstValueFrom to throw an error + (firstValueFrom as jest.Mock).mockRejectedValue(new Error('Network Error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.playPreviousTrack(accessToken, refreshToken, deviceId)).rejects.toThrow( + new HttpException("Failed to play next track", HttpStatus.INTERNAL_SERVER_ERROR), + ); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error playing previous track:', 'Network Error'); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); describe('getTrackDuration', () => { - it('should return the track duration', async () => { - const mockResponse = { data: { item: { duration_ms: 200000 } } }; - jest.spyOn(httpService, 'get').mockReturnValue(of(mockResponse) as any); - - const result = await service.getTrackDuration('accessToken', 'refreshToken'); - expect(result).toBe(200000); + it('should return track duration successfully', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + const mockDurationMs = 180000; // 3 minutes + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API with track duration + const mockResponse = { + data: { + item: { + duration_ms: mockDurationMs, + }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the GET request response + (lastValueFrom as jest.Mock).mockResolvedValue(mockResponse); + + // Act + const result = await service.getTrackDuration(accessToken, refreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(lastValueFrom).toHaveBeenCalledTimes(1); + expect(result).toBe(mockDurationMs); + }); + + it('should throw HttpException if access token is missing', async () => { + // Act & Assert + await expect(service.getTrackDuration('', 'mockRefreshToken')).rejects.toThrow( + new HttpException("Access token is missing while attempting to fetch track duration", HttpStatus.BAD_REQUEST), + ); + }); + + it('should throw an error if track duration is not present in response', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API with no track duration + const mockResponse = { + data: { + item: {}, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the GET request response + (lastValueFrom as jest.Mock).mockResolvedValue(mockResponse); + + // Act & Assert + await expect(service.getTrackDuration(accessToken, refreshToken)).rejects.toThrow( + new Error("Unable to fetch track duration"), + ); + }); + + it('should handle errors and throw an internal server error', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue('mockProviderToken'); + + // Mock lastValueFrom to throw an error + (lastValueFrom as jest.Mock).mockRejectedValue(new Error('Network Error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.getTrackDuration(accessToken, refreshToken)).rejects.toThrow( + new HttpException("Error fetching track duration", HttpStatus.INTERNAL_SERVER_ERROR), + ); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching track duration:', 'Network Error'); + + // Clean up + consoleErrorSpy.mockRestore(); }); }); describe('seekToPosition', () => { - it('should seek to a specific position in a track', async () => { - const mockResponse = { status: 204 }; - jest.spyOn(httpService, 'put').mockReturnValue(of(mockResponse) as any); - - const result = await service.seekToPosition('accessToken', 'refreshToken', 60000, 'deviceId'); + it('should update seek position successfully', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const position_ms = 60000; // 1 minute + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API when updating seek position + const mockResponse = { + data: {}, + status: 204, // Status 204 indicates a successful request with no content + statusText: 'No Content', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the PUT request response + (lastValueFrom as jest.Mock).mockResolvedValue(mockResponse); + + // Act + const result = await service.seekToPosition(accessToken, refreshToken, position_ms, deviceId); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(lastValueFrom).toHaveBeenCalledTimes(1); expect(result).toEqual({ message: 'Seek position updated successfully' }); }); + + it('should handle failure when updating seek position', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const position_ms = 60000; // 1 minute + const deviceId = 'mockDeviceId'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock a response with a status other than 204 to simulate a failure + const mockErrorResponse = { + data: {}, + status: 400, + statusText: 'Bad Request', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the PUT request failure + (lastValueFrom as jest.Mock).mockResolvedValue(mockErrorResponse); + + // Act & Assert + await expect(service.seekToPosition(accessToken, refreshToken, position_ms, deviceId)).rejects.toThrow( + new HttpException('Failed to update seek position', HttpStatus.BAD_REQUEST), + ); + }); + + it('should handle errors and throw an internal server error', async () => { + // Arrange + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const position_ms = 60000; // 1 minute + const deviceId = 'mockDeviceId'; + + // Mock the getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.seekToPosition(accessToken, refreshToken, position_ms, deviceId)).rejects.toThrow( + new HttpException('Error seeking to position', HttpStatus.INTERNAL_SERVER_ERROR), + ); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error seeking to position:', 'Token error'); + + // Clean up + consoleErrorSpy.mockRestore(); + }); }); describe('addToQueue', () => { - it('should add a track to the queue', async () => { - const mockResponse = { status: 204 }; - jest.spyOn(httpService, 'post').mockReturnValue(of(mockResponse) as any); - - const result = await service.addToQueue('spotify:track:trackId', 'deviceId', 'accessToken', 'refreshToken'); + it('should add a song to the queue successfully', async () => { + // Arrange + const uri = 'spotify:track:mockTrackId'; + const device_id = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the response from Spotify API when adding to the queue + const mockResponse = { + data: {}, + status: 204, // Status 204 indicates a successful request with no content + statusText: 'No Content', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the POST request response + (lastValueFrom as jest.Mock).mockResolvedValue(mockResponse); + + // Act + const result = await service.addToQueue(uri, device_id, accessToken, refreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(lastValueFrom).toHaveBeenCalledTimes(1); expect(result).toEqual({ message: 'Song added to queue successfully' }); }); + + it('should handle failure when adding a song to the queue', async () => { + // Arrange + const uri = 'spotify:track:mockTrackId'; + const device_id = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock a response with a status other than 204 to simulate a failure + const mockErrorResponse = { + data: {}, + status: 400, + statusText: 'Bad Request', + headers: {}, + config: {}, + }; + + // Mock lastValueFrom to simulate the POST request failure + (lastValueFrom as jest.Mock).mockResolvedValue(mockErrorResponse); + + // Act & Assert + await expect(service.addToQueue(uri, device_id, accessToken, refreshToken)).rejects.toThrow( + new HttpException('Failed to add Song to queue', HttpStatus.BAD_REQUEST), + ); + }); + + it('should handle errors and throw an internal server error', async () => { + // Arrange + const uri = 'spotify:track:mockTrackId'; + const device_id = 'mockDeviceId'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + + // Mock the getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect(service.addToQueue(uri, device_id, accessToken, refreshToken)).rejects.toThrow( + new HttpException('Error adding to queue.', HttpStatus.INTERNAL_SERVER_ERROR), + ); + + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error adding to queue:', 'Token error'); + + // Clean up + consoleErrorSpy.mockRestore(); + }); }); describe('getTrackDetailsByName', () => { - it('should return track details by name', async () => { - const mockSearchResponse = { - data: { - tracks: { items: [{ id: 'track1', name: 'Test Track' }] }, + it('should fetch and return track details by name', async () => { + // Arrange + const artistName = 'Artist'; + const trackName = 'Track'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the exposed getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock the search response from Spotify API + const mockSearchResponse = { + data: { + tracks: { + items: [ + { + id: 'mockTrackId', + name: 'Mock Track', + artists: [{ name: 'Mock Artist' }], + album: { + name: 'Mock Album', + images: [{ url: 'mockImageUrl' }], + }, + external_urls: { spotify: 'mockSpotifyUrl' }, + preview_url: 'mockPreviewUrl', + }, + ], }, - }; - const mockTrackResponse = { - data: { - id: 'track1', - name: 'Test Track', - album: { name: 'Test Album', images: [{ url: 'test-image-url' }] }, - artists: [{ name: 'Test Artist' }], - preview_url: 'test-preview-url', - external_urls: { spotify: 'test-spotify-url' }, + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; + + // Mock the track details response from Spotify API + const mockTrackDetailsResponse= { + data: { + id: 'mockTrackId', + name: 'Mock Track', + artists: [{ name: 'Mock Artist' }], + album: { + name: 'Mock Album', + images: [{ url: 'mockAlbumImageUrl' }], }, - }; + external_urls: { spotify: 'mockTrackSpotifyUrl' }, + preview_url: 'mockTrackPreviewUrl', + }, + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + }; - jest.spyOn(httpService, 'get').mockImplementation((url: string) => { - if (url.includes('search')) { - return of(mockSearchResponse) as any; - } - return of(mockTrackResponse) as any; - }); + // Mock lastValueFrom to simulate responses for the search and track details + (lastValueFrom as jest.Mock) + .mockResolvedValueOnce(mockSearchResponse) // First call - search response + .mockResolvedValueOnce(mockTrackDetailsResponse); // Second call - track details response + + // Act + const result = await service.getTrackDetailsByName( + artistName, + trackName, + accessToken, + refreshToken, + ); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(accessToken, refreshToken); + expect(lastValueFrom).toHaveBeenCalledTimes(2); + expect(result).toEqual({ + id: 'mockTrackId', + name: 'Mock Track', + albumName: 'Mock Album', + albumImageUrl: 'mockAlbumImageUrl', + artistName: 'Mock Artist', + previewUrl: 'mockTrackPreviewUrl', + spotifyUrl: 'mockTrackSpotifyUrl', + }); + }); + + it('should handle errors gracefully and throw the error', async () => { + // Arrange + const artistName = 'Artist'; + const trackName = 'Track'; + const accessToken = 'mockAccessToken'; + const refreshToken = 'mockRefreshToken'; + + // Mock the exposed getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act & Assert + await expect( + service.getTrackDetailsByName(artistName, trackName, accessToken, refreshToken), + ).rejects.toThrow('Token error'); - const result = await service.getTrackDetailsByName('Test Artist', 'Test Track', 'accessToken', 'refreshToken'); + // Assert error handling + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching track details:', expect.any(Error)); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + }); + + + describe('getTrackAnalyisis', () => { + it('should fetch and return track analysis', async () => { + // Arrange + const mockTrackId = 'mockTrackId'; + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the exposed getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock lastValueFrom to return a fake response + (lastValueFrom as jest.Mock).mockResolvedValue({ + data: { + valence: 0.5, + energy: 0.7, + danceability: 0.8, + tempo: 120, + }, + }); + + // Act + const result = await service.getTrackAnalysis(mockTrackId, mockAccessToken, mockRefreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(lastValueFrom).toHaveBeenCalledWith(expect.anything()); expect(result).toEqual({ - id: 'track1', - name: 'Test Track', - albumName: 'Test Album', - albumImageUrl: 'test-image-url', - artistName: 'Test Artist', - previewUrl: 'test-preview-url', - spotifyUrl: 'test-spotify-url', + valence: 0.5, + energy: 0.7, + danceability: 0.8, + tempo: 120, }); }); + + it('should handle errors gracefully', async () => { + // Arrange + const mockTrackId = 'mockTrackId'; + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + + // Mock the exposed getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act + const result = await service.getTrackAnalysis(mockTrackId, mockAccessToken, mockRefreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching track analysis:', expect.any(Error)); + expect(result).toBeUndefined(); + + // Clean up + consoleErrorSpy.mockRestore(); + }); }); + describe('getTopTracks', () => { + it('should fetch and return top tracks', async () => { + // Arrange + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + + // Mock the exposed getAccessToken method to return a fake provider token + jest.spyOn(service, 'getAccessToken').mockResolvedValue(mockProviderToken); + + // Mock lastValueFrom to return a fake response + (lastValueFrom as jest.Mock).mockResolvedValue({ + data: { + items: [ + { + id: '1', + name: 'Track 1', + album: { + name: 'Album 1', + images: [{ url: 'http://album1.image.url' }], + }, + artists: [{ name: 'Artist 1' }], + preview_url: 'http://preview1.url', + external_urls: { spotify: 'http://spotify.url/track1' }, + }, + { + id: '2', + name: 'Track 2', + album: { + name: 'Album 2', + images: [{ url: 'http://album2.image.url' }], + }, + artists: [{ name: 'Artist 2' }], + preview_url: 'http://preview2.url', + external_urls: { spotify: 'http://spotify.url/track2' }, + }, + ], + }, + }); + + // Act + const result = await service.getTopTracks(mockAccessToken, mockRefreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(lastValueFrom).toHaveBeenCalledWith(expect.anything()); + expect(result).toEqual([ + { + id: '1', + name: 'Track 1', + albumName: 'Album 1', + albumImageUrl: 'http://album1.image.url', + artistName: 'Artist 1', + previewUrl: 'http://preview1.url', + spotifyUrl: 'http://spotify.url/track1', + }, + { + id: '2', + name: 'Track 2', + albumName: 'Album 2', + albumImageUrl: 'http://album2.image.url', + artistName: 'Artist 2', + previewUrl: 'http://preview2.url', + spotifyUrl: 'http://spotify.url/track2', + }, + ]); + }); + + it('should handle errors gracefully', async () => { + // Arrange + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + + // Mock the exposed getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act + const result = await service.getTopTracks(mockAccessToken, mockRefreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching top tracks:', expect.any(Error)); + expect(result).toBeUndefined(); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + }); + describe('getTopArtists', () => { + it('should retrieve top artists', async () => { + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + const mockProviderToken = 'mockProviderToken'; + (lastValueFrom as jest.Mock).mockResolvedValue( + { + data: { + items: [ + { + id: '1', + name: 'Artist 1', + images: [{ url: 'http://image1.url' }], + uri: 'spotify:artist:1', + }, + { + id: '2', + name: 'Artist 2', + images: [{ url: 'http://image2.url' }], + uri: 'spotify:artist:2', + }, + ] + } + } + ); + + const result = await service.getTopArtists(mockAccessToken, mockRefreshToken); + + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(lastValueFrom).toHaveBeenCalledWith( + expect.anything() // You can specify the exact expected call if needed + ); + expect(result).toEqual([ + { id: '1', name: 'Artist 1', imageUrl: 'http://image1.url', spotifyUrl: 'spotify:artist:1' }, + { id: '2', name: 'Artist 2', imageUrl: 'http://image2.url', spotifyUrl: 'spotify:artist:2' }, + ]); + }); + + it('should handle errors gracefully', async () => { + // Arrange + const mockAccessToken = 'mockAccessToken'; + const mockRefreshToken = 'mockRefreshToken'; + + // Mock the exposed getAccessToken method to throw an error + jest.spyOn(service, 'getAccessToken').mockRejectedValue(new Error('Token error')); + + // Mock console.error to suppress error logs in tests + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Act + const result = await service.getTopArtists(mockAccessToken, mockRefreshToken); + + // Assert + expect(service.getAccessToken).toHaveBeenCalledWith(mockAccessToken, mockRefreshToken); + expect(consoleErrorSpy).toHaveBeenCalledWith('Error fetching top artists:', expect.any(Error)); + expect(result).toBeUndefined(); + + // Clean up + consoleErrorSpy.mockRestore(); + }); + }); diff --git a/Backend/src/spotify/services/spotify.service.ts b/Backend/src/spotify/services/spotify.service.ts index c44cb545..e3b356dd 100644 --- a/Backend/src/spotify/services/spotify.service.ts +++ b/Backend/src/spotify/services/spotify.service.ts @@ -39,7 +39,7 @@ export class SpotifyService } // This function retrieves the access key (for the Clustering recommendations) from the config file - private async getAccessKey(): Promise + protected async getAccessKey(): Promise { try { @@ -114,7 +114,7 @@ export class SpotifyService } // This function fetches the tracks from the Spotify API based on the given trackIDs (a string of comma-delimited track IDs) - private async fetchSpotifyTracks(trackIds: string, accessToken: string, refreshToken: string): Promise + protected async fetchSpotifyTracks(trackIds: string, accessToken: string, refreshToken: string): Promise { const providerToken = await this.getAccessToken(accessToken, refreshToken); const response = await lastValueFrom( From f69562e9c0fd2f63b411252dacb1b97bc9419373 Mon Sep 17 00:00:00 2001 From: Tristan Potgieter Date: Sat, 7 Sep 2024 14:12:34 +0200 Subject: [PATCH 09/51] =?UTF-8?q?=F0=9F=94=B0=2086%=20coverage=20for=20bac?= =?UTF-8?q?kend=20achieved.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit encryptionKey member variable changed from private to protected for testability purposes. --- .../services/supabase.service.spec.ts | 319 ++++++++---------- .../src/supabase/services/supabase.service.ts | 2 +- .../controller/youtube.controller.spec.ts | 156 ++++++--- .../youtube/services/youtube.service.spec.ts | 233 +++++++------ 4 files changed, 373 insertions(+), 337 deletions(-) diff --git a/Backend/src/supabase/services/supabase.service.spec.ts b/Backend/src/supabase/services/supabase.service.spec.ts index 41cba5b7..7e7b7207 100644 --- a/Backend/src/supabase/services/supabase.service.spec.ts +++ b/Backend/src/supabase/services/supabase.service.spec.ts @@ -3,208 +3,161 @@ import { createSupabaseClient } from './supabaseClient'; import * as crypto from 'crypto'; jest.mock('./supabaseClient', () => ({ - createSupabaseClient: jest.fn(), + createSupabaseClient: jest.fn(), })); -describe('SupabaseService', () => { - let supabaseService: SupabaseService; +jest.mock("../../config", () => ({ + encryptionKey: "dGVzdGVuY3J5cHRpb25rZXk=", // Example base64-encoded string + })); + +jest.mock('crypto', () => ({ + randomBytes: jest.fn().mockReturnValue(Buffer.from('randombytes', 'utf-8')), + createCipheriv: jest.fn(() => ({ + update: jest.fn().mockReturnValue('encrypted_part'), + final: jest.fn().mockReturnValue('encrypted_final'), + })), + createDecipheriv: jest.fn(() => ({ + update: jest.fn().mockReturnValue('decrypted_part'), + final: jest.fn().mockReturnValue('decrypted_final'), + })), +})); - beforeEach(() => { - process.env.SECRET_ENCRYPTION_KEY = Buffer.from('test-key').toString('base64'); - supabaseService = new SupabaseService(); +describe('SupabaseService', () => { + let service: SupabaseService; + let supabaseMock: any; + + beforeEach(() => { + service = new SupabaseService(); + supabaseMock = { + auth: { + signInWithOAuth: jest.fn(), + exchangeCodeForSession: jest.fn(), + setSession: jest.fn(), + getUser: jest.fn(), + }, + from: jest.fn().mockReturnThis(), + upsert: jest.fn(), + select: jest.fn(), + eq: jest.fn().mockReturnThis(), + single: jest.fn(), + }; + (createSupabaseClient as jest.Mock).mockReturnValue(supabaseMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('signinWithOAuth', () => { + it('should sign in with OAuth using the given provider', async () => { + supabaseMock.auth.signInWithOAuth.mockResolvedValue({ data: { url: 'http://test-url' }, error: null }); + const url = await service.signinWithOAuth('spotify'); + expect(supabaseMock.auth.signInWithOAuth).toHaveBeenCalledWith({ + provider: 'spotify', + options: { + redirectTo: 'http://localhost:4200/auth/callback', + scopes: expect.any(String), + }, + }); + expect(url).toBe('http://test-url'); }); - afterEach(() => { - jest.clearAllMocks(); + it('should throw an error if signInWithOAuth fails', async () => { + supabaseMock.auth.signInWithOAuth.mockResolvedValue({ data: null, error: { message: 'OAuth error' } }); + await expect(service.signinWithOAuth('spotify')).rejects.toThrow('OAuth error'); }); + }); - describe('signInWithSpotifyOAuth', () => { - it('should call supabase.auth.signInWithOAuth and return the URL', async () => { - const mockSupabase = { - auth: { - signInWithOAuth: jest.fn().mockResolvedValue({ data: { url: 'http://localhost' }, error: null }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - const result = await supabaseService.signinWithOAuth("Spotify"); - expect(mockSupabase.auth.signInWithOAuth).toHaveBeenCalledWith({ - provider: 'Spotify', - options: { - redirectTo: 'http://localhost:4200/auth/callback', - scopes: '', - }, - }); - expect(result).toBe('http://localhost'); - }); - - it('should throw an error if signInWithOAuth fails', async () => { - const mockSupabase = { - auth: { - signInWithOAuth: jest.fn().mockResolvedValue({ data: null, error: { message: 'OAuth error' } }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await expect(supabaseService.signinWithOAuth("Spotify")).rejects.toThrow('OAuth error'); - }); + describe('exchangeCodeForSession', () => { + it('should exchange the code for a session', async () => { + supabaseMock.auth.exchangeCodeForSession.mockResolvedValue({ error: null }); + await expect(service.exchangeCodeForSession('test_code')).resolves.not.toThrow(); + expect(supabaseMock.auth.exchangeCodeForSession).toHaveBeenCalledWith('test_code'); }); - describe('exchangeCodeForSession', () => { - it('should call supabase.auth.exchangeCodeForSession and handle success', async () => { - const mockSupabase = { - auth: { - exchangeCodeForSession: jest.fn().mockResolvedValue({ error: null }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await supabaseService.exchangeCodeForSession('test-code'); - expect(mockSupabase.auth.exchangeCodeForSession).toHaveBeenCalledWith('test-code'); - }); - - it('should throw an error if exchangeCodeForSession fails', async () => { - const mockSupabase = { - auth: { - exchangeCodeForSession: jest.fn().mockResolvedValue({ error: { message: 'Session error' } }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await expect(supabaseService.exchangeCodeForSession('test-code')).rejects.toThrow('Session error'); - }); + it('should throw an error if exchangeCodeForSession fails', async () => { + supabaseMock.auth.exchangeCodeForSession.mockResolvedValue({ error: { message: 'Session error' } }); + await expect(service.exchangeCodeForSession('test_code')).rejects.toThrow('Session error'); }); + }); - describe('handleSpotifyTokens', () => { - /* - it('should set session and insert tokens on success', async () => { - const mockSupabase = { - auth: { - setSession: jest.fn().mockResolvedValue({ error: null }), - getUser: jest.fn().mockResolvedValue({ data: { user: { id: 'test-user' } }, error: null }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - const insertTokensSpy = jest.spyOn(supabaseService, 'insertTokens').mockResolvedValue(); - - await supabaseService.handleSpotifyTokens('access', 'refresh', 'provider', 'providerRefresh'); - expect(mockSupabase.auth.setSession).toHaveBeenCalledWith({ access_token: 'access', refresh_token: 'refresh' }); - expect(insertTokensSpy).toHaveBeenCalledWith('test-user', expect.any(String), expect.any(String)); - }); - - */ - it('should log error if setSession fails', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const mockSupabase = { - auth: { - setSession: jest.fn().mockResolvedValue({ error: 'Session error' }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await supabaseService.handleSpotifyTokens('access', 'refresh', 'provider', 'providerRefresh'); - expect(consoleSpy).toHaveBeenCalledWith('Error setting session:', 'Session error'); - }); - - it('should log error if getUser fails', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - const mockSupabase = { - auth: { - setSession: jest.fn().mockResolvedValue({ error: null }), - getUser: jest.fn().mockResolvedValue({ data: null, error: 'User error' }), - }, - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await supabaseService.handleSpotifyTokens('access', 'refresh', 'provider', 'providerRefresh'); - expect(consoleSpy).toHaveBeenCalledWith('Error retrieving user:', 'User error'); - }); - }); + describe('handleSpotifyTokens', () => { + it('should handle tokens and insert them into user_tokens table', async () => { + supabaseMock.auth.setSession.mockResolvedValue({ error: null }); + supabaseMock.auth.getUser.mockResolvedValue({ data: { user: { id: 'user_id' } }, error: null }); + const insertTokensSpy = jest.spyOn(service, 'insertTokens').mockResolvedValue(); + + await service.handleSpotifyTokens('access', 'refresh', 'providerToken', 'providerRefreshToken'); - describe('insertTokens', () => { - it('should insert tokens into the database', async () => { - const mockSupabase = { - from: jest.fn().mockReturnThis(), - upsert: jest.fn().mockResolvedValue({ data: {}, error: null }), - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await supabaseService.insertTokens('test-user', 'encryptedToken', 'encryptedRefreshToken'); - expect(mockSupabase.from).toHaveBeenCalledWith('user_tokens'); - expect(mockSupabase.upsert).toHaveBeenCalledWith([ - { - user_id: 'test-user', - encrypted_provider_token: 'encryptedToken', - encrypted_provider_refresh_token: 'encryptedRefreshToken', - }, - ], { - onConflict: 'user_id', - }); - }); - - it('should throw an error if upsert fails', async () => { - const mockSupabase = { - from: jest.fn().mockReturnThis(), - upsert: jest.fn().mockResolvedValue({ data: null, error: 'Insert error' }), - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await expect(supabaseService.insertTokens('test-user', 'encryptedToken', 'encryptedRefreshToken')).rejects.toThrow('Failed to update or insert tokens'); - }); + expect(supabaseMock.auth.setSession).toHaveBeenCalledWith({ + access_token: 'access', + refresh_token: 'refresh', + }); + expect(insertTokensSpy).toHaveBeenCalledWith('user_id', expect.any(String), expect.any(String)); }); - describe('encryptToken', () => { - /* - it('should encrypt a token', () => { - const token = 'test-token'; - const encryptedToken = supabaseService.encryptToken(token); + it('should return error message if tokens are missing', async () => { + const response = await service.handleSpotifyTokens('', '', '', ''); + expect(response).toEqual({ + message: 'Error occurred during OAuth Sign In while processing tokens - please try again.', + }); + }); + }); + + describe('insertTokens', () => { + it('should upsert tokens into user_tokens table', async () => { + supabaseMock.upsert.mockResolvedValue({ data: 'inserted_data', error: null }); + await service.insertTokens('user_id', 'encrypted_provider_token', 'encrypted_provider_refresh_token'); + expect(supabaseMock.upsert).toHaveBeenCalledWith( + [ + { + user_id: 'user_id', + encrypted_provider_token: 'encrypted_provider_token', + encrypted_provider_refresh_token: 'encrypted_provider_refresh_token', + }, + ], + { onConflict: 'user_id' }, + ); + }); - expect(encryptedToken).toContain(':'); // Check if the result contains the IV and encrypted token parts - }); - */ + it('should throw an error if upsert fails', async () => { + supabaseMock.upsert.mockResolvedValue({ data: null, error: { message: 'Insert error' } }); + await expect( + service.insertTokens('user_id', 'encrypted_provider_token', 'encrypted_provider_refresh_token'), + ).rejects.toThrow('Failed to update or insert tokens'); }); + }); - describe('decryptToken', () => { - /* - it('should decrypt a token', () => { - const token = 'test-token'; - const encryptedToken = supabaseService.encryptToken(token); - const decryptedToken = supabaseService.decryptToken(encryptedToken); + describe('encryptToken', () => { + it('should encrypt a token', () => { + const encrypted = service.encryptToken('test_token'); + expect(crypto.createCipheriv).toHaveBeenCalled(); + expect(encrypted).toContain(':'); + }); + }); - expect(decryptedToken).toBe(token); - }); - */ + describe('decryptToken', () => { + it('should decrypt an encrypted token', () => { + const decrypted = service.decryptToken('iv:encrypted'); + expect(crypto.createDecipheriv).toHaveBeenCalled(); + expect(decrypted).toBe('decrypted_partdecrypted_final'); + }); + }); + + describe('retrieveTokens', () => { + it('should retrieve and decrypt tokens from user_tokens table', async () => { + supabaseMock.single.mockResolvedValue({ + data: { encrypted_provider_token: 'encrypted', encrypted_provider_refresh_token: 'encrypted' }, + error: null, + }); + const tokens = await service.retrieveTokens('user_id'); + expect(supabaseMock.select).toHaveBeenCalledWith('encrypted_provider_token, encrypted_provider_refresh_token'); + expect(tokens).toEqual({ providerToken: 'decrypted_partdecrypted_final', providerRefreshToken: 'decrypted_partdecrypted_final' }); }); - describe('retrieveTokens', () => { - it('should retrieve and decrypt tokens from the database', async () => { - const mockSupabase = { - from: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - eq: jest.fn().mockReturnThis(), - single: jest.fn().mockResolvedValue({ data: { encrypted_provider_token: 'encryptedToken', encrypted_provider_refresh_token: 'encryptedRefreshToken' }, error: null }), - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - const decryptTokenSpy = jest.spyOn(supabaseService, 'decryptToken').mockReturnValueOnce('providerToken').mockReturnValueOnce('providerRefreshToken'); - - const tokens = await supabaseService.retrieveTokens('test-user'); - expect(tokens).toEqual({ providerToken: 'providerToken', providerRefreshToken: 'providerRefreshToken' }); - expect(decryptTokenSpy).toHaveBeenCalledWith('encryptedToken'); - expect(decryptTokenSpy).toHaveBeenCalledWith('encryptedRefreshToken'); - }); - - it('should throw an error if retrieval fails', async () => { - const mockSupabase = { - from: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - eq: jest.fn().mockReturnThis(), - single: jest.fn().mockResolvedValue({ data: null, error: 'Retrieve error' }), - }; - (createSupabaseClient as jest.Mock).mockReturnValue(mockSupabase); - - await expect(supabaseService.retrieveTokens('test-user')).rejects.toThrow('Failed to retrieve tokens'); - }); + it('should throw an error if retrieval fails', async () => { + supabaseMock.single.mockResolvedValue({ data: null, error: { message: 'Retrieval error' } }); + await expect(service.retrieveTokens('user_id')).rejects.toThrow('Failed to retrieve tokens'); }); + }); }); diff --git a/Backend/src/supabase/services/supabase.service.ts b/Backend/src/supabase/services/supabase.service.ts index 239a71ab..c20dc8d9 100644 --- a/Backend/src/supabase/services/supabase.service.ts +++ b/Backend/src/supabase/services/supabase.service.ts @@ -6,7 +6,7 @@ import * as crypto from "crypto"; @Injectable() export class SupabaseService { - private encryptionKey: Buffer; + protected encryptionKey: Buffer; constructor() { this.encryptionKey = Buffer.from(encryptionKey, "base64"); diff --git a/Backend/src/youtube/controller/youtube.controller.spec.ts b/Backend/src/youtube/controller/youtube.controller.spec.ts index e7265521..f717adcb 100644 --- a/Backend/src/youtube/controller/youtube.controller.spec.ts +++ b/Backend/src/youtube/controller/youtube.controller.spec.ts @@ -1,68 +1,130 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { YoutubeController } from './youtube.controller'; -import { YoutubeService, YouTubeTrackInfo } from '../services/youtube.service'; +import { YouTubeController } from './youtube.controller'; +import { YouTubeService } from '../services/youtube.service'; +import { HttpException, HttpStatus } from '@nestjs/common'; -describe('YoutubeController', () => { - let controller: YoutubeController; - let youtubeService: YoutubeService; +describe('YouTubeController', () => { + let controller: YouTubeController; + let youtubeService: YouTubeService; + + const mockYouTubeService = { + searchVideos: jest.fn(), + getVideoDetails: jest.fn(), + getAPIKey: jest.fn(), + }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [YoutubeController], + controllers: [YouTubeController], providers: [ { - provide: YoutubeService, - useValue: { - getQueue: jest.fn(), - fetchSingleTrackDetails: jest.fn(), - }, + provide: YouTubeService, + useValue: mockYouTubeService, }, ], }).compile(); - controller = module.get(YoutubeController); - youtubeService = module.get(YoutubeService); + controller = module.get(YouTubeController); + youtubeService = module.get(YouTubeService); }); - it('should be defined', () => { - expect(controller).toBeDefined(); + afterEach(() => { + jest.clearAllMocks(); }); - describe('getQueue', () => { - it('should return the result of youtubeService.getQueue', async () => { - const mockResult: YouTubeTrackInfo[] = [{ - id: 'someId', - title: 'Some Title', - thumbnailUrl: 'http://example.com/thumbnail.jpg', - channelTitle: 'Some Channel', - videoUrl: 'http://example.com/video' - }]; - const body = { artist: 'Artist Name', song_name: 'Song Name' }; - jest.spyOn(youtubeService, 'getQueue').mockResolvedValue(mockResult); - - const result = await controller.getQueue(body); - - expect(result).toBe(mockResult); - expect(youtubeService.getQueue).toHaveBeenCalledWith(body.artist, body.song_name); + describe('search', () => { + it('should throw an error if access token or refresh token is missing', async () => { + await expect( + controller.search({ accessToken: '', refreshToken: '', query: 'test' }), + ).rejects.toThrow( + new HttpException( + 'Access token or refresh token is missing while attempting to search for a song using YouTube.', + HttpStatus.UNAUTHORIZED, + ), + ); + }); + + it('should throw an error if query is missing', async () => { + await expect( + controller.search({ accessToken: 'valid', refreshToken: 'valid', query: '' }), + ).rejects.toThrow( + new HttpException( + 'Query is missing while attempting to search for videos on YouTube.', + HttpStatus.BAD_REQUEST, + ), + ); + }); + + it('should call youtubeService.searchVideos with the correct query', async () => { + mockYouTubeService.searchVideos.mockResolvedValue(['video1', 'video2']); + const result = await controller.search({ + accessToken: 'valid', + refreshToken: 'valid', + query: 'test', + }); + + expect(mockYouTubeService.searchVideos).toHaveBeenCalledWith('test'); + expect(result).toEqual(['video1', 'video2']); }); }); - describe('getTrackDetails', () => { - it('should return the result of youtubeService.fetchSingleTrackDetails', async () => { - const mockResult: YouTubeTrackInfo = { - id: 'someId', - title: 'Some Title', - thumbnailUrl: 'http://example.com/thumbnail.jpg', - channelTitle: 'Some Channel', - videoUrl: 'http://example.com/video' - }; - const body = { videoId: '123456' }; - jest.spyOn(youtubeService, 'fetchSingleTrackDetails').mockResolvedValue(mockResult); - - const result = await controller.getTrackDetails(body); - - expect(result).toBe(mockResult); - expect(youtubeService.fetchSingleTrackDetails).toHaveBeenCalledWith(body.videoId); + describe('getVideo', () => { + it('should throw an error if access token or refresh token is missing', async () => { + await expect( + controller.getVideo({ accessToken: '', refreshToken: '', id: '123' }), + ).rejects.toThrow( + new HttpException( + "Access token or refresh token is missing while attempting to retrieve a song's details on YouTube.", + HttpStatus.UNAUTHORIZED, + ), + ); + }); + + it('should throw an error if video ID is missing', async () => { + await expect( + controller.getVideo({ accessToken: 'valid', refreshToken: 'valid', id: '' }), + ).rejects.toThrow( + new HttpException( + 'Video ID is missing while attempting to retrieve video details from YouTube.', + HttpStatus.BAD_REQUEST, + ), + ); + }); + + it('should call youtubeService.getVideoDetails with the correct video ID', async () => { + mockYouTubeService.getVideoDetails.mockResolvedValue({ id: '123', title: 'Test Video' }); + const result = await controller.getVideo({ + accessToken: 'valid', + refreshToken: 'valid', + id: '123', + }); + + expect(mockYouTubeService.getVideoDetails).toHaveBeenCalledWith('123'); + expect(result).toEqual({ id: '123', title: 'Test Video' }); + }); + }); + + describe('getKey', () => { + it('should throw an error if access token or refresh token is missing', async () => { + await expect( + controller.getKey({ accessToken: '', refreshToken: '' }), + ).rejects.toThrow( + new HttpException( + 'Access token or refresh token is missing while attempting to retrieve a YouTube API key.', + HttpStatus.UNAUTHORIZED, + ), + ); + }); + + it('should call youtubeService.getAPIKey', async () => { + mockYouTubeService.getAPIKey.mockResolvedValue('API_KEY'); + const result = await controller.getKey({ + accessToken: 'valid', + refreshToken: 'valid', + }); + + expect(mockYouTubeService.getAPIKey).toHaveBeenCalled(); + expect(result).toBe('API_KEY'); }); }); }); diff --git a/Backend/src/youtube/services/youtube.service.spec.ts b/Backend/src/youtube/services/youtube.service.spec.ts index 6983ee80..a6a4b9f6 100644 --- a/Backend/src/youtube/services/youtube.service.spec.ts +++ b/Backend/src/youtube/services/youtube.service.spec.ts @@ -1,162 +1,183 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { HttpService, HttpModule } from '@nestjs/axios'; -import { of, throwError } from 'rxjs'; -import { YoutubeService, YouTubeTrackInfo } from './youtube.service'; -import { HttpException, HttpStatus } from '@nestjs/common'; -import { AxiosResponse } from 'axios'; - -describe('YoutubeService', () => { - let service: YoutubeService; +import { YouTubeService, TrackInfo } from './youtube.service'; +import { HttpService } from '@nestjs/axios'; +import { of } from 'rxjs'; + +describe('YouTubeService', () => { + let service: YouTubeService; let httpService: HttpService; + const mockHttpService = { + get: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [HttpModule], - providers: [YoutubeService], + providers: [ + YouTubeService, + { + provide: HttpService, + useValue: mockHttpService, + }, + ], }).compile(); - service = module.get(YoutubeService); + service = module.get(YouTubeService); httpService = module.get(HttpService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + afterEach(() => { + jest.clearAllMocks(); }); - describe('getQueue', () => { - - it('should return YouTubeTrackInfo array on successful fetch', async () => { - const artist = 'Artist'; - const songName = 'SongName'; - const mockEchoResponse: AxiosResponse = { - data: { - recommended_tracks: [ - { track_details: ['Track1', 'Artist1'] }, - { track_details: ['Track2', 'Artist2'] }, - ], + describe('mapYouTubeResponseToTrackInfo', () => { + it('should map YouTube API response to TrackInfo', () => { + const youtubeResponse = { + id: { videoId: '123' }, + snippet: { + title: 'Test Video', + album: 'Test Album', + thumbnails: { high: { url: 'https://example.com/image.jpg' } }, + channelTitle: 'Test Artist', }, - status: 200, - statusText: 'OK', - headers: {}, - config: { - headers: undefined + }; + + const expected: TrackInfo = { + id: '123', + name: 'Test Video', + albumName: 'Test Album', + albumImageUrl: 'https://example.com/image.jpg', + artistName: 'Test Artist', + previewUrl: '', + youtubeId: '123', + }; + + const result = service.mapYouTubeResponseToTrackInfo(youtubeResponse); + expect(result).toEqual(expected); + }); + + it('should default albumName to "Unknown Album" if not provided', () => { + const youtubeResponse = { + id: { videoId: '456' }, + snippet: { + title: 'No Album Video', + thumbnails: { high: { url: 'https://example.com/noalbum.jpg' } }, + channelTitle: 'Unknown Artist', }, }; - const mockYoutubeResponse: AxiosResponse = { + + const expected: TrackInfo = { + id: '456', + name: 'No Album Video', + albumName: 'Unknown Album', + albumImageUrl: 'https://example.com/noalbum.jpg', + artistName: 'Unknown Artist', + previewUrl: '', + youtubeId: '456', + }; + + const result = service.mapYouTubeResponseToTrackInfo(youtubeResponse); + expect(result).toEqual(expected); + }); + }); + + describe('searchVideos', () => { + it('should search for videos and return mapped results', async () => { + const youtubeResponse = { data: { items: [ { - id: 'videoId1', + id: { videoId: '123' }, snippet: { - title: 'Track1', - thumbnails: { default: { url: 'url1' } }, - channelTitle: 'Channel1', + title: 'Video 1', + thumbnails: { high: { url: 'https://example.com/vid1.jpg' } }, + channelTitle: 'Artist 1', }, }, { - id: 'videoId2', + id: { videoId: '456' }, snippet: { - title: 'Track2', - thumbnails: { default: { url: 'url2' } }, - channelTitle: 'Channel2', + title: 'Video 2', + thumbnails: { high: { url: 'https://example.com/vid2.jpg' } }, + channelTitle: 'Artist 2', }, }, ], }, - status: 200, - statusText: 'OK', - headers: {}, - config: { - headers: undefined - }, }; - jest.spyOn(httpService, 'post').mockReturnValueOnce(of(mockEchoResponse)); - jest.spyOn(httpService, 'get').mockReturnValue(of(mockYoutubeResponse)); + mockHttpService.get.mockReturnValue(of(youtubeResponse)); - const result = await service.getQueue(artist, songName); + const result = await service.searchVideos('test query'); + expect(mockHttpService.get).toHaveBeenCalledWith( + expect.stringContaining( + `${service['API_URL']}/search?part=snippet&q=test%20query&key=${service['API_KEY']}` + ) + ); expect(result).toEqual([ { - id: 'videoId1', - title: 'Track1', - thumbnailUrl: 'url1', - channelTitle: 'Channel1', - videoUrl: 'https://www.youtube.com/watch?v=videoId1', + id: '123', + name: 'Video 1', + albumName: 'Unknown Album', + albumImageUrl: 'https://example.com/vid1.jpg', + artistName: 'Artist 1', + previewUrl: '', + youtubeId: '123', }, { - id: 'videoId2', - title: 'Track2', - thumbnailUrl: 'url2', - channelTitle: 'Channel2', - videoUrl: 'https://www.youtube.com/watch?v=videoId2', + id: '456', + name: 'Video 2', + albumName: 'Unknown Album', + albumImageUrl: 'https://example.com/vid2.jpg', + artistName: 'Artist 2', + previewUrl: '', + youtubeId: '456', }, ]); }); - -/* - it('should throw an exception on error', async () => { - const artist = 'Artist'; - const songName = 'SongName'; - - jest.spyOn(httpService, 'post').mockReturnValueOnce(throwError(new Error('Echo API error'))); - - await expect(service.getQueue(artist, songName)).rejects.toThrow( - new HttpException('Failed to fetch queue', HttpStatus.INTERNAL_SERVER_ERROR), - ); - - }); - */ }); - - describe('fetchSingleTrackDetails', () => { - - it('should return YouTubeTrackInfo on successful fetch', async () => { - const videoId = 'videoId1'; - const mockYoutubeResponse: AxiosResponse = { + describe('getVideoDetails', () => { + it('should retrieve video details and map the response', async () => { + const youtubeResponse = { data: { items: [ { - id: 'videoId1', + id: { videoId: '789' }, snippet: { - title: 'Track1', - thumbnails: { default: { url: 'url1' } }, - channelTitle: 'Channel1', + title: 'Detailed Video', + thumbnails: { high: { url: 'https://example.com/detail.jpg' } }, + channelTitle: 'Detail Artist', }, }, ], }, - status: 200, - statusText: 'OK', - headers: {}, - config: { - headers: undefined - }, }; - - jest.spyOn(httpService, 'get').mockReturnValueOnce(of(mockYoutubeResponse)); - const result = await service.fetchSingleTrackDetails(videoId); + mockHttpService.get.mockReturnValue(of(youtubeResponse)); + + const result = await service.getVideoDetails('789'); + expect(mockHttpService.get).toHaveBeenCalledWith( + expect.stringContaining( + `${service['API_URL']}/videos?part=snippet,contentDetails,statistics&id=789&key=${service['API_KEY']}` + ) + ); expect(result).toEqual({ - id: 'videoId1', - title: 'Track1', - thumbnailUrl: 'url1', - channelTitle: 'Channel1', - videoUrl: 'https://www.youtube.com/watch?v=videoId1', + id: '789', + name: 'Detailed Video', + albumName: 'Unknown Album', + albumImageUrl: 'https://example.com/detail.jpg', + artistName: 'Detail Artist', + previewUrl: '', + youtubeId: '789', }); }); - - - /* - it('should throw an exception on error', async () => { - const videoId = 'videoId1'; - - jest.spyOn(httpService, 'get').mockReturnValueOnce(throwError(new Error('YouTube API error'))); + }); - await expect(service.fetchSingleTrackDetails(videoId)).rejects.toThrow( - new HttpException('Failed to fetch YouTube track details', HttpStatus.INTERNAL_SERVER_ERROR), - ); + describe('getAPIKey', () => { + it('should return the API key', async () => { + const result = await service.getAPIKey(); + expect(result).toBe(process.env.YOUTUBE_KEY); }); - */ }); }); From 69b692fa60369d0da1252b48cdb7c7d41be9fe11 Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:28:19 +0200 Subject: [PATCH 10/51] =?UTF-8?q?=F0=9F=93=90Switch=20mood=20list=20Accent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../button-component.component.html | 2 +- .../app/pages/profile/profile.component.html | 6 +- .../src/app/services/mood-service.service.ts | 56 +++++++++---------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Frontend/src/app/components/atoms/button-component/button-component.component.html b/Frontend/src/app/components/atoms/button-component/button-component.component.html index 44ce7d07..1cf97b87 100644 --- a/Frontend/src/app/components/atoms/button-component/button-component.component.html +++ b/Frontend/src/app/components/atoms/button-component/button-component.component.html @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/Frontend/src/app/pages/profile/profile.component.html b/Frontend/src/app/pages/profile/profile.component.html index 2c87ad3b..1fa67bab 100644 --- a/Frontend/src/app/pages/profile/profile.component.html +++ b/Frontend/src/app/pages/profile/profile.component.html @@ -48,13 +48,13 @@

Top Artists
-
+
-
+
+ class="rounded-lg overflow-hidden h-full relative">
diff --git a/Frontend/src/app/services/mood-service.service.ts b/Frontend/src/app/services/mood-service.service.ts index 0b2bb337..b8028730 100644 --- a/Frontend/src/app/services/mood-service.service.ts +++ b/Frontend/src/app/services/mood-service.service.ts @@ -101,34 +101,34 @@ export class MoodService { Surprise: 'bg-surprise text-surprise-text focus:ring-surprise-dark fill-surprise-dark transition-colors duration-mood ease-in-out', }; private _MoodClassesDark = { - Anger: 'bg-anger-dark transition-colors duration-mood ease-in-out', - Admiration: 'bg-admiration-dark transition-colors duration-mood ease-in-out', - Fear: 'bg-fear-dark transition-colors duration-mood ease-in-out', - Joy: 'bg-joy-dark transition-colors duration-mood ease-in-out', - Neutral: 'bg-default-dark transition-colors ', - Amusement: 'bg-amusement-dark transition-colors duration-mood ease-in-out', - Annoyance: 'bg-annoyance-dark transition-colors duration-mood ease-in-out', - Approval: 'bg-approval-dark transition-colors duration-mood ease-in-out', - Caring: 'bg-caring-dark transition-colors duration-mood ease-in-out', - Confusion: 'bg-confusion-dark transition-colors duration-mood ease-in-out', - Curiosity: 'bg-curiosity-dark transition-colors duration-mood ease-in-out', - Desire: 'bg-desire-dark transition-colors duration-mood ease-in-out', - Disappointment: 'bg-disappointment-dark transition-colors duration-mood ease-in-out', - Disapproval: 'bg-disapproval-dark transition-colors duration-mood ease-in-out', - Disgust: 'bg-disgust-dark transition-colors duration-mood ease-in-out', - Embarrassment: 'bg-embarrassment-dark transition-colors duration-mood ease-in-out', - Excitement: 'bg-excitement-dark transition-colors duration-mood ease-in-out', - Gratitude: 'bg-gratitude-dark transition-colors duration-mood ease-in-out', - Grief: 'bg-grief-dark transition-colors duration-mood ease-in-out', - Love: 'bg-love-dark transition-colors duration-mood ease-in-out', - Nervousness: 'bg-nervousness-dark transition-colors duration-mood ease-in-out', - Optimism: 'bg-optimism-dark transition-colors duration-mood ease-in-out', - Pride: 'bg-pride-dark transition-colors duration-mood ease-in-out', - Realisation: 'bg-realisation-dark transition-colors duration-mood ease-in-out', - Relief: 'bg-relief-dark transition-colors duration-mood ease-in-out', - Remorse: 'bg-remorse-dark transition-colors duration-mood ease-in-out', - Sadness: 'bg-sadness-dark transition-colors duration-mood ease-in-out', - Surprise: 'bg-surprise-dark transition-colors duration-mood ease-in-out', + Anger: 'bg-anger-dark text-gray-light transition-colors duration-mood ease-in-out', + Admiration: 'bg-admiration-dark text-gray-light transition-colors duration-mood ease-in-out', + Fear: 'bg-fear-dark text-gray-light transition-colors duration-mood ease-in-out', + Joy: 'bg-joy-dark text-gray-light transition-colors duration-mood ease-in-out', + Neutral: 'bg-default-dark text-gray-light transition-colors ', + Amusement: 'bg-amusement-dark text-gray-light transition-colors duration-mood ease-in-out', + Annoyance: 'bg-annoyance-dark text-gray-light transition-colors duration-mood ease-in-out', + Approval: 'bg-approval-dark text-gray-light transition-colors duration-mood ease-in-out', + Caring: 'bg-caring-dark text-gray-light transition-colors duration-mood ease-in-out', + Confusion: 'bg-confusion-dark text-gray-light transition-colors duration-mood ease-in-out', + Curiosity: 'bg-curiosity-dark text-gray-light transition-colors duration-mood ease-in-out', + Desire: 'bg-desire-dark text-gray-light transition-colors duration-mood ease-in-out', + Disappointment: 'bg-disappointment-dark text-gray-light transition-colors duration-mood ease-in-out', + Disapproval: 'bg-disapproval-dark text-gray-light transition-colors duration-mood ease-in-out', + Disgust: 'bg-disgust-dark text-gray-light transition-colors duration-mood ease-in-out', + Embarrassment: 'bg-embarrassment-dark text-gray-light transition-colors duration-mood ease-in-out', + Excitement: 'bg-excitement-dark text-gray-light transition-colors duration-mood ease-in-out', + Gratitude: 'bg-gratitude-dark text-gray-light transition-colors duration-mood ease-in-out', + Grief: 'bg-grief-dark text-gray-light transition-colors duration-mood ease-in-out', + Love: 'bg-love-dark text-gray-light transition-colors duration-mood ease-in-out', + Nervousness: 'bg-nervousness-dark text-gray-light transition-colors duration-mood ease-in-out', + Optimism: 'bg-optimism-dark text-gray-light transition-colors duration-mood ease-in-out', + Pride: 'bg-pride-dark text-gray-light transition-colors duration-mood ease-in-out', + Realisation: 'bg-realisation-dark text-gray-light transition-colors duration-mood ease-in-out', + Relief: 'bg-relief-dark text-gray-light transition-colors duration-mood ease-in-out', + Remorse: 'bg-remorse-dark text-gray-light transition-colors duration-mood ease-in-out', + Sadness: 'bg-sadness-dark text-gray-light transition-colors duration-mood ease-in-out', + Surprise: 'bg-surprise-dark text-gray-light transition-colors duration-mood ease-in-out', }; private _backgroundMoodClasses = { From fa5e68aae988f0b02dbe852a5c7d4ad995d0973a Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Mon, 9 Sep 2024 17:59:47 +0200 Subject: [PATCH 11/51] =?UTF-8?q?=F0=9F=94=A5Remove=20console.log=20statem?= =?UTF-8?q?ents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/app.component.ts | 1 - .../components/molecules/moods-list/moods-list.component.ts | 4 +--- .../app/components/molecules/top-card/top-card.component.ts | 1 - .../app/components/organisms/side-bar/side-bar.component.ts | 2 +- .../templates/desktop/other-nav/other-nav.component.ts | 2 -- Frontend/src/app/pages/profile/profile.component.ts | 1 - 6 files changed, 2 insertions(+), 9 deletions(-) diff --git a/Frontend/src/app/app.component.ts b/Frontend/src/app/app.component.ts index d9d28dd8..6c58053d 100644 --- a/Frontend/src/app/app.component.ts +++ b/Frontend/src/app/app.component.ts @@ -73,7 +73,6 @@ export class AppComponent implements OnInit { this.router.events.pipe( filter((event): event is NavigationEnd => event instanceof NavigationEnd) ).subscribe((event: NavigationEnd) => { - console.log('Navigation ended:', event.urlAfterRedirects); this.isAuthRoute = ['/login', '/register'].includes(event.urlAfterRedirects); this.isCallbackRoute = ['/auth/callback'].includes(event.urlAfterRedirects); }); diff --git a/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts b/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts index 425457b8..124ca2e2 100644 --- a/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts +++ b/Frontend/src/app/components/molecules/moods-list/moods-list.component.ts @@ -19,9 +19,7 @@ export class MoodsListComponent implements OnInit { this.redirectToMoodPage.emit(mood); } ngOnInit(): void { - for (let mood of this.moods){ - console.log(mood); - } + } } \ No newline at end of file diff --git a/Frontend/src/app/components/molecules/top-card/top-card.component.ts b/Frontend/src/app/components/molecules/top-card/top-card.component.ts index 7164b133..e5bbf2e3 100644 --- a/Frontend/src/app/components/molecules/top-card/top-card.component.ts +++ b/Frontend/src/app/components/molecules/top-card/top-card.component.ts @@ -37,7 +37,6 @@ export class TopCardComponent { }); dialogRef.afterClosed().subscribe((result) => { - console.log('The dialog was closed'); }); } } diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts b/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts index 5eb58b64..ffe5d04d 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts @@ -131,7 +131,7 @@ export class SideBarComponent implements OnInit { try { this.isLoading = true; const data = await this.spotifyService.getRecentlyPlayedTracks(this.provider); - console.log("Recently Played Tracks Data:", data); + console.log("First track: ", data.items[0]); data.items.forEach((item: any) => { const trackId = item.track.id; if (!this.recentListeningCardData.find(track => track.id === trackId)) { diff --git a/Frontend/src/app/components/templates/desktop/other-nav/other-nav.component.ts b/Frontend/src/app/components/templates/desktop/other-nav/other-nav.component.ts index 3855d115..486d5e6d 100644 --- a/Frontend/src/app/components/templates/desktop/other-nav/other-nav.component.ts +++ b/Frontend/src/app/components/templates/desktop/other-nav/other-nav.component.ts @@ -43,9 +43,7 @@ export class OtherNavComponent implements AfterViewInit { } toggleDropdown(): void { - console.log('Profile picture clicked. Toggling dropdown...'); this.isDropdownOpen = !this.isDropdownOpen; - console.log('Dropdown open:', this.isDropdownOpen); } @HostListener('document:click', ['$event']) diff --git a/Frontend/src/app/pages/profile/profile.component.ts b/Frontend/src/app/pages/profile/profile.component.ts index c730eedc..1c523f0a 100644 --- a/Frontend/src/app/pages/profile/profile.component.ts +++ b/Frontend/src/app/pages/profile/profile.component.ts @@ -100,7 +100,6 @@ export class ProfileComponent implements AfterViewInit dialogRef.afterClosed().subscribe((result) => { - console.log("The dialog was closed"); }); } From daa71a2e499e7349ff133f55529ff65d80b2ba2d Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:00:19 +0200 Subject: [PATCH 12/51] =?UTF-8?q?=F0=9F=93=90Refactor=20mood=20list=20comp?= =?UTF-8?q?onent=20to=20fix=20selected=20mood=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/molecules/mood-list/mood-list.component.html | 2 +- .../app/components/molecules/mood-list/mood-list.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Frontend/src/app/components/molecules/mood-list/mood-list.component.html b/Frontend/src/app/components/molecules/mood-list/mood-list.component.html index 28c6617e..c21d2f5c 100644 --- a/Frontend/src/app/components/molecules/mood-list/mood-list.component.html +++ b/Frontend/src/app/components/molecules/mood-list/mood-list.component.html @@ -2,7 +2,7 @@
\ No newline at end of file diff --git a/Frontend/src/app/components/molecules/mood-list/mood-list.component.ts b/Frontend/src/app/components/molecules/mood-list/mood-list.component.ts index 31652e30..91cf3ee5 100644 --- a/Frontend/src/app/components/molecules/mood-list/mood-list.component.ts +++ b/Frontend/src/app/components/molecules/mood-list/mood-list.component.ts @@ -12,9 +12,9 @@ import { ButtonComponentComponent } from './../../atoms/button-component/button- }) export class MoodListComponent { @Input() moods!: string[]; - @Input() selectedMood!: number | null; + @Input() selectedMood = 0; @Input() screenSize!: string; - + constructor(public moodService: MoodService) { } From 7e7bfca9feeb7ca9cc95d768da5c748e60f428ac Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:39:36 +0200 Subject: [PATCH 13/51] =?UTF-8?q?=F0=9F=93=90Refactor=20grid=20layout=20in?= =?UTF-8?q?=20app=20component=20and=20side=20bar=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/app.component.html | 2 +- .../components/organisms/side-bar/side-bar.component.html | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Frontend/src/app/app.component.html b/Frontend/src/app/app.component.html index 3434a832..bb120d11 100644 --- a/Frontend/src/app/app.component.html +++ b/Frontend/src/app/app.component.html @@ -4,7 +4,7 @@
-
+
diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html index fe42dfb1..e10ed144 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html @@ -1,12 +1,12 @@ -
+
- +
- +
From 9bdf34049f0c43367220734a0ef75610ec7f5bed Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:37:48 +0200 Subject: [PATCH 14/51] =?UTF-8?q?=F0=9F=9A=A7created=20a=20plus=20icon=20f?= =?UTF-8?q?rom=20svg=20icon=20for=20exapading=20side=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../atoms/svg-icon/svg-icon.component.ts | 2 +- .../desktop/left/left.component.html | 8 +++++ .../templates/desktop/left/left.component.ts | 33 ++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.ts b/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.ts index a17a5cb0..8fb73bf4 100644 --- a/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.ts +++ b/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.ts @@ -16,7 +16,7 @@ export class SvgIconComponent { @Input() fillColor: string = '#000000'; @Input() selected: boolean = false; @Input() isAnimating: boolean = false; - @Input() middleColor: string = '#FFFFFF'; + @Input() middleColor: string = '#191716'; @Input() pathHeight: string = '1'; // Default path height as a string @Input() circleAnimation: boolean = false; @Output() svgClick = new EventEmitter(); diff --git a/Frontend/src/app/components/templates/desktop/left/left.component.html b/Frontend/src/app/components/templates/desktop/left/left.component.html index 696eef7e..bd7236e1 100644 --- a/Frontend/src/app/components/templates/desktop/left/left.component.html +++ b/Frontend/src/app/components/templates/desktop/left/left.component.html @@ -1,2 +1,10 @@ + + + + diff --git a/Frontend/src/app/components/templates/desktop/left/left.component.ts b/Frontend/src/app/components/templates/desktop/left/left.component.ts index 73b88314..e458a085 100644 --- a/Frontend/src/app/components/templates/desktop/left/left.component.ts +++ b/Frontend/src/app/components/templates/desktop/left/left.component.ts @@ -1,27 +1,36 @@ +// left.component.ts import { Component } from '@angular/core'; import { NavbarComponent } from './../../../organisms/navbar/navbar.component'; import { SideBarComponent } from './../../../organisms/side-bar/side-bar.component'; -import { CommonModule} from "@angular/common"; +import { CommonModule } from "@angular/common"; import { AuthService } from "../../../../services/auth.service"; import { ProviderService } from "../../../../services/provider.service"; +import { SvgIconComponent } from '../../../atoms/svg-icon/svg-icon.component'; +import { MoodService } from "../../../../services/mood-service.service"; +const SVG_PATHS = { + PLUS: 'M20 0 H30 V20 H50 V30 H30 V50 H20 V30 H0 V20 H20 Z', + MIN: 'M0 20 H50 V30 H0 Z', +}; @Component({ selector: 'app-left', standalone: true, - imports: [NavbarComponent, SideBarComponent, CommonModule], + imports: [NavbarComponent, SideBarComponent, CommonModule, SvgIconComponent], templateUrl: './left.component.html', styleUrl: './left.component.css' }) export class LeftComponent { - constructor(private authService: AuthService, private providerService: ProviderService){} - //Check whether the app is ready to load data from Spotify - async ready() - { - if (this.providerService.getProviderName() === 'spotify') - { - return await this.authService.isReady(); - } - return false; + constructor(private moodService: MoodService) {} + + svgString: string = SVG_PATHS.PLUS; + isSideBarHidden = true; + + toggleSideBar() { + this.isSideBarHidden = !this.isSideBarHidden; } -} + + handleSvgClick(event: MouseEvent) { + console.log('SVG icon clicked', event); + } +} \ No newline at end of file From ed4cadd0d9330af70a02e7ed5dcaddd6522b9a0e Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:14:29 +0200 Subject: [PATCH 15/51] =?UTF-8?q?=F0=9F=8E=89Create=20expandable=20icon=20?= =?UTF-8?q?component=20for=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expandable-icon.component.css | 0 .../expandable-icon.component.html | 6 +++++ .../expandable-icon.component.spec.ts | 23 +++++++++++++++++++ .../expandable-icon.component.ts | 23 +++++++++++++++++++ .../desktop/left/left.component.html | 10 +------- .../templates/desktop/left/left.component.ts | 12 ++++------ 6 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.css create mode 100644 Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html create mode 100644 Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.spec.ts create mode 100644 Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.css b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.css new file mode 100644 index 00000000..e69de29b diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html new file mode 100644 index 00000000..fc14ad93 --- /dev/null +++ b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.html @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.spec.ts b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.spec.ts new file mode 100644 index 00000000..8e1dc0ac --- /dev/null +++ b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ExpandableIconComponent } from './expandable-icon.component'; + +describe('ExpandableIconComponent', () => { + let component: ExpandableIconComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExpandableIconComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExpandableIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts new file mode 100644 index 00000000..787c3479 --- /dev/null +++ b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { CommonModule } from "@angular/common"; +import { SvgIconComponent } from '../../atoms/svg-icon/svg-icon.component'; +const SVG_PATHS = { + PLUS: 'M20 0 H30 V20 H50 V30 H30 V50 H20 V30 H0 V20 H20 Z', + MIN: 'M0 20 H50 V30 H0 Z', +}; + +@Component({ + selector: 'app-expandable-icon', + standalone: true, + imports: [CommonModule,SvgIconComponent], + templateUrl: './expandable-icon.component.html', + styleUrl: './expandable-icon.component.css' +}) +export class ExpandableIconComponent { + + svgString: string = SVG_PATHS.PLUS; + + handleSvgClick(event: MouseEvent) { + this.svgString = this.svgString === SVG_PATHS.PLUS ? SVG_PATHS.MIN : SVG_PATHS.PLUS; + } +} diff --git a/Frontend/src/app/components/templates/desktop/left/left.component.html b/Frontend/src/app/components/templates/desktop/left/left.component.html index bd7236e1..e3414949 100644 --- a/Frontend/src/app/components/templates/desktop/left/left.component.html +++ b/Frontend/src/app/components/templates/desktop/left/left.component.html @@ -1,10 +1,2 @@ - - - - - + \ No newline at end of file diff --git a/Frontend/src/app/components/templates/desktop/left/left.component.ts b/Frontend/src/app/components/templates/desktop/left/left.component.ts index e458a085..4a4a46da 100644 --- a/Frontend/src/app/components/templates/desktop/left/left.component.ts +++ b/Frontend/src/app/components/templates/desktop/left/left.component.ts @@ -1,23 +1,22 @@ -// left.component.ts import { Component } from '@angular/core'; import { NavbarComponent } from './../../../organisms/navbar/navbar.component'; import { SideBarComponent } from './../../../organisms/side-bar/side-bar.component'; import { CommonModule } from "@angular/common"; -import { AuthService } from "../../../../services/auth.service"; -import { ProviderService } from "../../../../services/provider.service"; import { SvgIconComponent } from '../../../atoms/svg-icon/svg-icon.component'; import { MoodService } from "../../../../services/mood-service.service"; +import { ExpandableIconComponent } from '../../../organisms/expandable-icon/expandable-icon.component'; const SVG_PATHS = { PLUS: 'M20 0 H30 V20 H50 V30 H30 V50 H20 V30 H0 V20 H20 Z', MIN: 'M0 20 H50 V30 H0 Z', }; + @Component({ selector: 'app-left', standalone: true, - imports: [NavbarComponent, SideBarComponent, CommonModule, SvgIconComponent], + imports: [NavbarComponent, SideBarComponent, CommonModule, SvgIconComponent,ExpandableIconComponent], templateUrl: './left.component.html', - styleUrl: './left.component.css' + styleUrls: ['./left.component.css'] }) export class LeftComponent { constructor(private moodService: MoodService) {} @@ -29,8 +28,7 @@ export class LeftComponent { this.isSideBarHidden = !this.isSideBarHidden; } - handleSvgClick(event: MouseEvent) { - console.log('SVG icon clicked', event); + this.svgString = this.svgString === SVG_PATHS.PLUS ? SVG_PATHS.MIN : SVG_PATHS.PLUS; } } \ No newline at end of file From d2576bb981eb0136ed1aaabf8eb53c37ab0cb64b Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:30:24 +0200 Subject: [PATCH 16/51] =?UTF-8?q?=F0=9F=93=90Refactor=20SvgIconComponent?= =?UTF-8?q?=20to=20accept=20dynamic=20width?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/atoms/svg-icon/svg-icon.component.html | 2 +- .../app/components/atoms/svg-icon/svg-icon.component.ts | 1 + .../expandable-icon/expandable-icon.component.html | 1 + .../components/organisms/side-bar/side-bar.component.html | 7 +++++-- .../components/organisms/side-bar/side-bar.component.ts | 3 ++- .../components/templates/desktop/left/left.component.ts | 3 +-- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.html b/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.html index bdf8865e..f848e5a4 100644 --- a/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.html +++ b/Frontend/src/app/components/atoms/svg-icon/svg-icon.component.html @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html index e10ed144..a6b3f6ef 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html @@ -2,11 +2,14 @@
+
+ +
- +
- +
diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts b/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts index ffe5d04d..c5f8d0e6 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.ts @@ -12,13 +12,14 @@ import { SongCardsComponent } from "../song-cards/song-cards.component"; import { SearchService } from "../../../services/search.service"; import { SkeletonSongCardComponent } from "../../atoms/skeleton-song-card/skeleton-song-card.component"; import { ToastComponent } from '../../../components/organisms/toast/toast.component'; +import { ExpandableIconComponent } from '../../organisms/expandable-icon/expandable-icon.component'; type SelectedOption = 'suggestions' | 'recentListening'; @Component({ selector: "app-side-bar", standalone: true, - imports: [MatCard, MatCardContent, NgForOf, NgIf, NgClass, EchoButtonComponent, SongCardsComponent, SkeletonSongCardComponent, ToastComponent], + imports: [MatCard, MatCardContent, NgForOf, NgIf, NgClass, EchoButtonComponent, SongCardsComponent, SkeletonSongCardComponent, ToastComponent,ExpandableIconComponent], templateUrl: "./side-bar.component.html", styleUrls: ["./side-bar.component.css"] }) diff --git a/Frontend/src/app/components/templates/desktop/left/left.component.ts b/Frontend/src/app/components/templates/desktop/left/left.component.ts index 4a4a46da..b7a8d2c0 100644 --- a/Frontend/src/app/components/templates/desktop/left/left.component.ts +++ b/Frontend/src/app/components/templates/desktop/left/left.component.ts @@ -4,7 +4,6 @@ import { SideBarComponent } from './../../../organisms/side-bar/side-bar.compone import { CommonModule } from "@angular/common"; import { SvgIconComponent } from '../../../atoms/svg-icon/svg-icon.component'; import { MoodService } from "../../../../services/mood-service.service"; -import { ExpandableIconComponent } from '../../../organisms/expandable-icon/expandable-icon.component'; const SVG_PATHS = { PLUS: 'M20 0 H30 V20 H50 V30 H30 V50 H20 V30 H0 V20 H20 Z', @@ -14,7 +13,7 @@ const SVG_PATHS = { @Component({ selector: 'app-left', standalone: true, - imports: [NavbarComponent, SideBarComponent, CommonModule, SvgIconComponent,ExpandableIconComponent], + imports: [NavbarComponent, SideBarComponent, CommonModule, SvgIconComponent], templateUrl: './left.component.html', styleUrls: ['./left.component.css'] }) From bd2a8c75257d3667d065a31610867d34e77367d9 Mon Sep 17 00:00:00 2001 From: Rueben van der Westhuizen <91849806+21434809@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:11:26 +0200 Subject: [PATCH 17/51] =?UTF-8?q?=F0=9F=93=90added=20side=20bar=20toggle?= =?UTF-8?q?=20on=20app.component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Frontend/src/app/app.component.html | 13 +++-- Frontend/src/app/app.component.ts | 17 +++++-- .../expandable-icon.component.ts | 14 ++++-- .../organisms/side-bar/side-bar.component.css | 6 +-- .../side-bar/side-bar.component.html | 20 +++++--- .../organisms/side-bar/side-bar.component.ts | 48 +++++++++---------- .../templates/desktop/left/left.component.ts | 16 ------- 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/Frontend/src/app/app.component.html b/Frontend/src/app/app.component.html index bb120d11..7dd23442 100644 --- a/Frontend/src/app/app.component.html +++ b/Frontend/src/app/app.component.html @@ -5,10 +5,15 @@
- + - -
+ + +
@@ -22,4 +27,4 @@
-
+
\ No newline at end of file diff --git a/Frontend/src/app/app.component.ts b/Frontend/src/app/app.component.ts index 6c58053d..ce1615a0 100644 --- a/Frontend/src/app/app.component.ts +++ b/Frontend/src/app/app.component.ts @@ -6,12 +6,13 @@ import { ScreenSizeService } from "./services/screen-size-service.service"; import { SwUpdate } from "@angular/service-worker"; import { filter } from "rxjs/operators"; import { CommonModule, isPlatformBrowser } from "@angular/common"; -import { SideBarComponent } from "./components/organisms/side-bar/side-bar.component"; import { ProviderService } from "./services/provider.service"; import { PageHeaderComponent } from "./components/molecules/page-header/page-header.component"; import { MoodService } from "./services/mood-service.service"; import { BackgroundAnimationComponent } from "./components/organisms/background-animation/background-animation.component"; +import { NavbarComponent } from "./components/organisms/navbar/navbar.component"; +import { SideBarComponent } from './components/organisms/side-bar/side-bar.component'; //template imports import { HeaderComponent } from "./components/organisms/header/header.component"; import { OtherNavComponent } from "./components/templates/desktop/other-nav/other-nav.component"; @@ -31,7 +32,9 @@ import { PlayerStateService } from "./services/player-state.service"; HeaderComponent, OtherNavComponent, LeftComponent, - BackgroundAnimationComponent + BackgroundAnimationComponent, + NavbarComponent, + SideBarComponent ], templateUrl: "./app.component.html", styleUrls: ["./app.component.css"] @@ -40,6 +43,9 @@ export class AppComponent implements OnInit { update: boolean = false; screenSize!: string; displayPageName: boolean = false; + columnStart: number = 3; + columnStartNav: number = 1; + protected displaySideBar: boolean = false; protected isAuthRoute: boolean = false; protected isCallbackRoute: boolean = false; @@ -77,7 +83,7 @@ export class AppComponent implements OnInit { this.isCallbackRoute = ['/auth/callback'].includes(event.urlAfterRedirects); }); } - + async ngOnInit() { this.screenSizeService.screenSize$.subscribe(screenSize => { this.screenSize = screenSize; @@ -91,10 +97,13 @@ export class AppComponent implements OnInit { return ['/login', '/register','/Auth/callback'].includes(this.router.url); } + layout(isSidebarOpen: boolean) { + this.columnStart = isSidebarOpen ? 1 : 3; + } isReady(): boolean { if (isPlatformBrowser(this.platformId)) return this.playerStateService.isReady(); return false; } -} +} \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts index 787c3479..cd1572ee 100644 --- a/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts +++ b/Frontend/src/app/components/organisms/expandable-icon/expandable-icon.component.ts @@ -1,6 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, Output, EventEmitter } from '@angular/core'; import { CommonModule } from "@angular/common"; import { SvgIconComponent } from '../../atoms/svg-icon/svg-icon.component'; + const SVG_PATHS = { PLUS: 'M20 0 H30 V20 H50 V30 H30 V50 H20 V30 H0 V20 H20 Z', MIN: 'M0 20 H50 V30 H0 Z', @@ -9,15 +10,18 @@ const SVG_PATHS = { @Component({ selector: 'app-expandable-icon', standalone: true, - imports: [CommonModule,SvgIconComponent], + imports: [CommonModule, SvgIconComponent], templateUrl: './expandable-icon.component.html', styleUrl: './expandable-icon.component.css' }) export class ExpandableIconComponent { - svgString: string = SVG_PATHS.PLUS; - + svgString: string = SVG_PATHS.MIN; + + @Output() svgClicked = new EventEmitter(); + handleSvgClick(event: MouseEvent) { this.svgString = this.svgString === SVG_PATHS.PLUS ? SVG_PATHS.MIN : SVG_PATHS.PLUS; + this.svgClicked.emit(); } -} +} \ No newline at end of file diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.css b/Frontend/src/app/components/organisms/side-bar/side-bar.component.css index 5d5ec93d..753ae9ae 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.css +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.css @@ -34,10 +34,7 @@ .scrollbar-hidden::-webkit-scrollbar { display: none; /* Safari and Chrome */ } -.container { - width: var(--tw-w); - height: var(--tw-h); -} + .view-more-container { /* Adjust the padding value based on your needs */ margin-bottom: calc(50vh - 50px); /* Assuming the button's height is around 100px */ @@ -47,4 +44,3 @@ color: inherit !important; /* Neutralizes text color change */ /* Add any other properties that change on hover to be neutralized */ } - diff --git a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html index a6b3f6ef..a9d592ae 100644 --- a/Frontend/src/app/components/organisms/side-bar/side-bar.component.html +++ b/Frontend/src/app/components/organisms/side-bar/side-bar.component.html @@ -1,19 +1,23 @@ -
-
+
+
+
- +
-
+ +
-
+
-
- + + +
+
@@ -25,10 +29,12 @@
+
+