Repository navigation
feat(bot): criativaria live twitch notification (poll every 2 min) #1613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
LucasSantana-Dev
merged 3 commits into
main
from
feat/bot-criativaria-live-notification
Jun 26, 2026
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
3889b72
feat(bot): criativaria live notification service (twitch polling)
LucasSantana-Dev eac7d8c
Merge branch 'main' into feat/bot-criativaria-live-notification
LucasSantana-Dev d35acbb
test(bot): cover fetchstream and exclude clienthandler from sonar
LucasSantana-Dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
225 changes: 225 additions & 0 deletions
225
packages/bot/src/services/CriativariaLiveNotificationService.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import { | ||
| describe, | ||
| test, | ||
| expect, | ||
| jest, | ||
| beforeEach, | ||
| afterEach, | ||
| } from '@jest/globals' | ||
| import type { Client, TextChannel } from 'discord.js' | ||
|
|
||
| jest.mock('@lucky/shared/utils', () => ({ | ||
| infoLog: jest.fn(), | ||
| errorLog: jest.fn(), | ||
| debugLog: jest.fn(), | ||
| warnLog: jest.fn(), | ||
| successLog: jest.fn(), | ||
| })) | ||
|
|
||
| jest.mock('../twitch/token') | ||
|
|
||
| import { CriativariaLiveNotificationService } from './CriativariaLiveNotificationService' | ||
| import { getTwitchUserAccessToken } from '../twitch/token' | ||
|
|
||
| const mockGetToken = getTwitchUserAccessToken as jest.MockedFunction< | ||
| typeof getTwitchUserAccessToken | ||
| > | ||
|
|
||
| describe('CriativariaLiveNotificationService', () => { | ||
| let service: CriativariaLiveNotificationService | ||
| let mockClient: Partial<Client> | ||
| let mockChannel: Partial<TextChannel> | ||
|
|
||
| beforeEach(() => { | ||
| mockChannel = { | ||
| send: jest.fn(), | ||
| } | ||
|
|
||
| mockClient = { | ||
| channels: { | ||
| fetch: jest.fn(async () => mockChannel), | ||
| }, | ||
| } | ||
|
|
||
| service = new CriativariaLiveNotificationService(() => Date.now(), 1000) | ||
| process.env.CRIATIVARIA_LIVES_CHANNEL_ID = 'test-channel-id' | ||
| process.env.CRIATIVARIA_TWITCH_USER_LOGIN = 'criativaria' | ||
| process.env.TWITCH_CLIENT_ID = 'test-client-id' | ||
| }) | ||
|
|
||
| afterEach(() => { | ||
| jest.clearAllMocks() | ||
| service.stop() | ||
| }) | ||
|
|
||
| test('should not start when env vars are missing', async () => { | ||
| delete process.env.CRIATIVARIA_LIVES_CHANNEL_ID | ||
| const service2 = new CriativariaLiveNotificationService( | ||
| () => Date.now(), | ||
| 1000, | ||
| ) | ||
| service2.start(mockClient as Client) | ||
| expect(mockClient.channels.fetch).not.toHaveBeenCalled() | ||
| service2.stop() | ||
| }) | ||
|
|
||
| test('should not notify when offline (no stream returned)', async () => { | ||
| jest.spyOn(service, 'fetchStream').mockResolvedValue(null) | ||
|
|
||
| await service.checkAndNotify(mockClient as Client) | ||
|
|
||
| expect(mockChannel.send).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| test('should notify when stream is live with new stream ID', async () => { | ||
| jest.spyOn(service, 'fetchStream').mockResolvedValue({ | ||
| id: 'stream-123', | ||
| user_login: 'criativaria', | ||
| title: 'Criativaria ao Vivo', | ||
| viewer_count: 1500, | ||
| game_name: 'Creative', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| }) | ||
|
|
||
| await service.checkAndNotify(mockClient as Client) | ||
|
|
||
| expect(mockChannel.send).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| test('should not re-notify for same stream ID', async () => { | ||
| jest.spyOn(service, 'fetchStream').mockResolvedValue({ | ||
| id: 'stream-123', | ||
| user_login: 'criativaria', | ||
| title: 'Criativaria ao Vivo', | ||
| viewer_count: 1500, | ||
| game_name: 'Creative', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| }) | ||
|
|
||
| await service.checkAndNotify(mockClient as Client) | ||
| expect(mockChannel.send).toHaveBeenCalledTimes(1) | ||
|
|
||
| await service.checkAndNotify(mockClient as Client) | ||
| expect(mockChannel.send).toHaveBeenCalledTimes(1) | ||
| }) | ||
|
|
||
| test('should clear lastNotifiedStreamId when stream goes offline', async () => { | ||
| jest.spyOn(service, 'fetchStream').mockResolvedValue({ | ||
| id: 'stream-123', | ||
| user_login: 'criativaria', | ||
| title: 'Criativaria ao Vivo', | ||
| viewer_count: 1500, | ||
| game_name: 'Creative', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| }) | ||
|
|
||
| await service.checkAndNotify(mockClient as Client) | ||
| expect(mockChannel.send).toHaveBeenCalledTimes(1) | ||
|
|
||
| jest.spyOn(service, 'fetchStream').mockResolvedValue(null) | ||
| await service.checkAndNotify(mockClient as Client) | ||
|
|
||
| jest.spyOn(service, 'fetchStream').mockResolvedValue({ | ||
| id: 'stream-456', | ||
| user_login: 'criativaria', | ||
| title: 'Criativaria ao Vivo 2', | ||
| viewer_count: 2000, | ||
| game_name: 'Creative', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| }) | ||
| await service.checkAndNotify(mockClient as Client) | ||
|
|
||
| expect(mockChannel.send).toHaveBeenCalledTimes(2) | ||
| }) | ||
|
|
||
| test('should handle send errors gracefully', async () => { | ||
| jest.spyOn(service, 'fetchStream').mockResolvedValue({ | ||
| id: 'stream-123', | ||
| user_login: 'criativaria', | ||
| title: 'Criativaria ao Vivo', | ||
| viewer_count: 1500, | ||
| game_name: 'Creative', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| }) | ||
|
|
||
| jest.mocked(mockChannel.send).mockRejectedValue( | ||
| new Error('Send failed'), | ||
| ) | ||
|
|
||
| await expect( | ||
| service.checkAndNotify(mockClient as Client), | ||
| ).resolves.toBeUndefined() | ||
| }) | ||
|
|
||
| test('should stop the interval when stop() is called', async () => { | ||
| service.start(mockClient as Client) | ||
|
|
||
| service.stop() | ||
|
|
||
| expect((service as any).intervalHandle).toBeNull() | ||
| }) | ||
|
|
||
| describe('fetchStream', () => { | ||
| beforeEach(() => { | ||
| mockGetToken.mockResolvedValue('test-token') | ||
| }) | ||
|
|
||
| test('returns null when no token', async () => { | ||
| mockGetToken.mockResolvedValue(null) | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('returns null when TWITCH_CLIENT_ID not set', async () => { | ||
| delete process.env.TWITCH_CLIENT_ID | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('returns null when fetch returns non-ok status', async () => { | ||
| global.fetch = jest.fn(async () => ({ ok: false })) as any | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('returns null when fetch throws', async () => { | ||
| global.fetch = jest.fn(async () => { | ||
| throw new Error('network error') | ||
| }) as any | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('returns null when data array is empty', async () => { | ||
| global.fetch = jest.fn(async () => ({ | ||
| ok: true, | ||
| json: async () => ({ data: [] }), | ||
| })) as any | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toBeNull() | ||
| }) | ||
|
|
||
| test('returns stream when fetch succeeds', async () => { | ||
| const stream = { | ||
| id: 'stream-1', | ||
| user_login: 'criativaria', | ||
| title: 'Live Test', | ||
| viewer_count: 100, | ||
| game_name: 'Gaming', | ||
| thumbnail_url: 'https://example.com/{width}x{height}.jpg', | ||
| started_at: new Date().toISOString(), | ||
| } | ||
| global.fetch = jest.fn(async () => ({ | ||
| ok: true, | ||
| json: async () => ({ data: [stream] }), | ||
| })) as any | ||
| const result = await service.fetchStream('criativaria') | ||
| expect(result).toEqual(stream) | ||
| }) | ||
| }) | ||
| }) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.