-
Notifications
You must be signed in to change notification settings - Fork 0
test: weekly coverage improvement 2026-06-14 #79
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
Open
vayungodara
wants to merge
4
commits into
main
Choose a base branch
from
test/coverage-2026-06-14
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
421106f
test: add unit tests for low-coverage lib files (2026-06-14)
claude 5d3130c
fix(test): handle ICU midnight hour variance in getHourInTimezone test
claude 4402ac9
chore: update package-lock.json
claude 32557d7
test: assert notification payloads per Codex review feedback
claude 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Coverage Report — 2026-06-14 | ||
|
|
||
| ## Summary | ||
|
|
||
| | Metric | Before | After | | ||
| |--------|--------|-------| | ||
| | Statements | 79.86% | 89.38% | | ||
| | Branches | 69.44% | 77.35% | | ||
| | Functions | 67.1% | 76.92% | | ||
| | Lines | 83.33% | 92.53% | | ||
|
|
||
| Total tests: 187 → 233 (+46 new tests). | ||
|
|
||
| ## Week-over-Week (vs 2026-04-12) | ||
|
|
||
| | Metric | 2026-04-12 | 2026-06-14 | Change | | ||
| |--------|-----------|-----------|--------| | ||
| | Statements | 74.9% | 89.38% | +14.48% | | ||
| | Branches | 67.66% | 77.35% | +9.69% | | ||
| | Functions | 62.5% | 76.92% | +14.42% | | ||
| | Lines | 78.05% | 92.53% | +14.48% | | ||
|
|
||
| - Files that were untested last report and are now covered: `comments.js` (0% → 100%), `nudges.js` (0% → 100%), `cronAuth.js` (0% → 100%), `email.js` (0% → 95.23%) | ||
| - `streaks.js` improved from 49% → 91.5% statements | ||
| - Files that remain untested across multiple weeks: `onboarding.js`, `partnerships.js`, `confetti.js`, `sounds.js` | ||
|
|
||
| ## Files Changed (last 7 days) | ||
|
|
||
| No lib/ files changed in the last 7 days. Targeted lowest-coverage and untested utility files instead. | ||
|
|
||
| ## New Tests Written | ||
|
|
||
| - `tests/unit/lib/streaks.test.js` — extended with 14 new tests, now covers: `formatUTCDate`, `formatDateInTimezone`, `getHourInTimezone`, `calculateStreak`, `getActivityHeatmap` | ||
| - `tests/unit/lib/comments.test.js` — 14 tests, covers: `getComments`, `getBatchCommentCounts`, `postComment`, `deleteComment` | ||
| - `tests/unit/lib/nudges.test.js` — 5 tests, covers: `sendNudge` (auth, rate limiting, happy path, insert error, missing profile) | ||
| - `tests/unit/lib/cronAuth.test.js` — 6 tests, covers: `verifyCronSecret` (valid token, missing header, missing env, wrong token, wrong length, empty header) | ||
| - `tests/unit/lib/email.test.js` — 6 tests, covers: `sendReminderEmail` (happy path, Resend error, network throw, correct recipient, missing userName, XSS escaping) | ||
|
|
||
| ## Mock Helper Update | ||
|
|
||
| - Added `limit` to `tests/setup/supabase-mock.js` chain methods, enabling tests for `nudges.js` (which was previously blocked by the missing `.limit()` support). | ||
|
|
||
| ## Skipped (out of scope) | ||
|
|
||
| - `lib/FocusContext.js`, `lib/NotificationContext.js`, `lib/KeyboardShortcutsContext.js` — React context providers | ||
| - `lib/animations.js` — pure animation data presets, no logic to test | ||
| - `lib/supabase/*` — Supabase client configuration | ||
| - `lib/useKeyboardShortcuts.js`, `lib/useModalScrollLock.js` — React hooks (require component rendering) | ||
| - `lib/pactTemplates.js`, `lib/accentColors.js` — already at 100% coverage | ||
| - `lib/onboarding.js` — requires `.upsert()` mock support (deferred) | ||
| - `lib/partnerships.js` — requires `.or()` and multi-chain builder states (deferred) | ||
| - `lib/confetti.js`, `lib/sounds.js` — browser-only side-effect modules (canvas/audio APIs) | ||
|
|
||
| ## Notes | ||
|
|
||
| - `streaks.js` uncovered line 131 is a `break` inside the current-streak loop when the gap is >1 day; hard to reach since the mock resolves both concurrent queries to the same value (would need per-call builder state). Lines 18-21 are the unexported `formatLocalDate` helper. | ||
| - `activity.js` function coverage remains at 29% because `fetchProfilesMap` (private), `isTestActivity` (private), and the full happy paths of `getGroupActivity`/`getAllActivity`/`getGroupStats` involve multiple sequential Supabase queries with profile/reaction assembly that the shared mock can't differentiate per-table. The exported functions are tested at their error and empty-data boundaries. | ||
| - `cronAuth.js` reached 100% coverage by adding a same-length-different-value token test that exercises the `timingSafeEqual` false branch. | ||
| - Next week's candidates for coverage improvement: `onboarding.js` and `partnerships.js` (require mock helper extension for `.upsert()` and `.or()`). |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import { describe, it, expect } from 'vitest'; | ||
| import { | ||
| getComments, | ||
| getBatchCommentCounts, | ||
| postComment, | ||
| deleteComment, | ||
| } from '@/lib/comments'; | ||
| import { createMockSupabase } from '../../setup/supabase-mock'; | ||
|
|
||
| describe('getComments', () => { | ||
| it('returns comments with user profiles attached', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValueSequence([ | ||
| { | ||
| data: [ | ||
| { id: 'c1', activity_id: 'a1', user_id: 'u1', comment_text: 'Nice!', created_at: '2024-06-15T12:00:00Z' }, | ||
| ], | ||
| error: null, | ||
| }, | ||
| { | ||
| data: [{ id: 'u1', full_name: 'Alice', avatar_url: 'alice.png' }], | ||
| error: null, | ||
| }, | ||
| ]); | ||
|
|
||
| const result = await getComments(supabase, 'a1'); | ||
| expect(result.error).toBeNull(); | ||
| expect(result.data).toHaveLength(1); | ||
| expect(result.data[0].comment_text).toBe('Nice!'); | ||
| expect(result.data[0].user.full_name).toBe('Alice'); | ||
| }); | ||
|
|
||
| it('returns empty array when no comments exist', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: [], error: null }); | ||
|
|
||
| const result = await getComments(supabase, 'a1'); | ||
| expect(result.data).toEqual([]); | ||
| expect(result.error).toBeNull(); | ||
| }); | ||
|
|
||
| it('returns empty array and error on DB failure', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: { message: 'DB error' } }); | ||
|
|
||
| const result = await getComments(supabase, 'a1'); | ||
| expect(result.data).toEqual([]); | ||
| expect(result.error).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('uses "Unknown" for missing profiles', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValueSequence([ | ||
| { | ||
| data: [{ id: 'c1', activity_id: 'a1', user_id: 'u-missing', comment_text: 'Hi' }], | ||
| error: null, | ||
| }, | ||
| { data: [], error: null }, | ||
| ]); | ||
|
|
||
| const result = await getComments(supabase, 'a1'); | ||
| expect(result.data[0].user.full_name).toBe('Unknown'); | ||
| expect(result.data[0].user.avatar_url).toBeNull(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getBatchCommentCounts', () => { | ||
| it('returns counts per activity', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ | ||
| data: [ | ||
| { activity_id: 'a1' }, | ||
| { activity_id: 'a1' }, | ||
| { activity_id: 'a2' }, | ||
| ], | ||
| error: null, | ||
| }); | ||
|
|
||
| const result = await getBatchCommentCounts(supabase, ['a1', 'a2']); | ||
| expect(result).toEqual({ a1: 2, a2: 1 }); | ||
| }); | ||
|
|
||
| it('returns empty object for empty activityIds', async () => { | ||
| const { supabase } = createMockSupabase(); | ||
| const result = await getBatchCommentCounts(supabase, []); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| it('returns empty object on DB error', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: { message: 'error' } }); | ||
|
|
||
| const result = await getBatchCommentCounts(supabase, ['a1']); | ||
| expect(result).toEqual({}); | ||
| }); | ||
|
|
||
| it('handles null data gracefully', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: null }); | ||
|
|
||
| const result = await getBatchCommentCounts(supabase, ['a1']); | ||
| expect(result).toEqual({}); | ||
| }); | ||
| }); | ||
|
|
||
| describe('postComment', () => { | ||
| it('returns not authenticated when no user', async () => { | ||
| const { supabase } = createMockSupabase(); | ||
| supabase.auth.getUser.mockResolvedValue({ data: { user: null }, error: null }); | ||
|
|
||
| const result = await postComment(supabase, 'a1', 'Hello'); | ||
| expect(result).toEqual({ success: false, error: 'Not authenticated' }); | ||
| }); | ||
|
|
||
| it('returns success on happy path', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValueSequence([ | ||
| { data: { id: 'c1', comment_text: 'Hello' }, error: null }, | ||
| { data: { user_id: 'other-user' }, error: null }, | ||
| { data: null, error: null }, | ||
| ]); | ||
|
|
||
| const result = await postComment(supabase, 'a1', 'Hello'); | ||
| expect(result.success).toBe(true); | ||
| expect(result.data).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('returns failure on insert error', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: { message: 'insert failed' } }); | ||
|
|
||
| const result = await postComment(supabase, 'a1', 'Hello'); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('truncates long comments in notification', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| const longText = 'A'.repeat(100); | ||
| builder.mockReturnValueSequence([ | ||
| { data: { id: 'c1', comment_text: longText }, error: null }, | ||
| { data: { user_id: 'other-user' }, error: null }, | ||
| { data: null, error: null }, | ||
| ]); | ||
|
|
||
| const result = await postComment(supabase, 'a1', longText); | ||
| expect(result.success).toBe(true); | ||
| }); | ||
| }); | ||
|
|
||
| describe('deleteComment', () => { | ||
| it('returns not authenticated when no user', async () => { | ||
| const { supabase } = createMockSupabase(); | ||
| supabase.auth.getUser.mockResolvedValue({ data: { user: null }, error: null }); | ||
|
|
||
| const result = await deleteComment(supabase, 'c1'); | ||
| expect(result).toEqual({ success: false, error: 'Not authenticated' }); | ||
| }); | ||
|
|
||
| it('returns success on happy path', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: null }); | ||
|
|
||
| const result = await deleteComment(supabase, 'c1'); | ||
| expect(result).toEqual({ success: true }); | ||
| }); | ||
|
|
||
| it('returns failure on delete error', async () => { | ||
| const { supabase, builder } = createMockSupabase(); | ||
| builder.mockReturnValue({ data: null, error: { message: 'delete failed' } }); | ||
|
|
||
| const result = await deleteComment(supabase, 'c1'); | ||
| expect(result.success).toBe(false); | ||
| expect(result.error).toBeTruthy(); | ||
| }); | ||
| }); | ||
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,59 @@ | ||
| import { describe, it, expect, beforeEach, afterEach } from 'vitest'; | ||
| import { verifyCronSecret } from '@/lib/cronAuth'; | ||
|
|
||
| describe('verifyCronSecret', () => { | ||
| const ORIGINAL_ENV = process.env; | ||
|
|
||
| beforeEach(() => { | ||
| process.env = { ...ORIGINAL_ENV, CRON_SECRET: 'my-secret-123' }; | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = ORIGINAL_ENV; | ||
| }); | ||
|
|
||
| it('returns authorized for correct bearer token', () => { | ||
| const req = { headers: { get: (key) => key === 'authorization' ? 'Bearer my-secret-123' : null } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(true); | ||
| expect(result.response).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('returns unauthorized when no authorization header', () => { | ||
| const req = { headers: { get: () => null } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(false); | ||
| expect(result.response).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('returns unauthorized when CRON_SECRET is not set', () => { | ||
| delete process.env.CRON_SECRET; | ||
| const req = { headers: { get: () => 'Bearer something' } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(false); | ||
| }); | ||
|
|
||
| it('returns unauthorized for token with different length', () => { | ||
| const req = { headers: { get: () => 'Bearer short' } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(false); | ||
| }); | ||
|
|
||
| it('returns unauthorized for wrong token of same length', () => { | ||
| const req = { headers: { get: () => 'Bearer my-secret-456' } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(false); | ||
| }); | ||
|
|
||
| it('returns unauthorized when header is empty string', () => { | ||
| const req = { headers: { get: () => '' } }; | ||
|
|
||
| const result = verifyCronSecret(req); | ||
| expect(result.authorized).toBe(false); | ||
| }); | ||
| }); |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When notification truncation regresses and
postCommentsends the complete 100-character comment, this test still passes because it only checks the function's success result. Inspect the notificationbuilder.insertcall and assert that itsmessagecontains exactly 60 characters plus the ellipsis so the behavior named by the test is protected.Useful? React with 👍 / 👎.