From 4fb851947e5b016a9c5e896f857c50ecc9ac8348 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 31 Mar 2026 16:35:16 +0200 Subject: [PATCH] test(auth): add 68 unit tests for auth-ui, shared-auth, and shared-branding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - userAgent utils: parseUserAgent, getDeviceType, formatUserAgent (17 tests) - guestWelcome utils: shouldShow, markSeen, reset (8 tests) - jwtUtils: decodeToken, isTokenValid, getUserFromToken, B2B (27 tests) - mana-apps: hasAppAccess, getTierLevel, getAccessibleManaApps (16 tests) Also fixes iOS detection bug in userAgent parser (iPhone UA contains "Mac OS X" — mobile check must come before desktop OS check). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/shared-auth-ui/package.json | 4 +- .../src/utils/guestWelcome.spec.ts | 92 ++++++++ .../src/utils/userAgent.spec.ts | 97 ++++++++ .../shared-auth-ui/src/utils/userAgent.ts | 8 +- packages/shared-auth-ui/vitest.config.js | 12 + packages/shared-auth/package.json | 2 + .../shared-auth/src/core/jwtUtils.spec.ts | 209 ++++++++++++++++++ packages/shared-auth/vitest.config.js | 12 + .../shared-branding/src/mana-apps.spec.ts | 124 +++++++++++ 9 files changed, 555 insertions(+), 5 deletions(-) create mode 100644 packages/shared-auth-ui/src/utils/guestWelcome.spec.ts create mode 100644 packages/shared-auth-ui/src/utils/userAgent.spec.ts create mode 100644 packages/shared-auth-ui/vitest.config.js create mode 100644 packages/shared-auth/src/core/jwtUtils.spec.ts create mode 100644 packages/shared-auth/vitest.config.js create mode 100644 packages/shared-branding/src/mana-apps.spec.ts diff --git a/packages/shared-auth-ui/package.json b/packages/shared-auth-ui/package.json index 64a889131..6bb44e0a0 100644 --- a/packages/shared-auth-ui/package.json +++ b/packages/shared-auth-ui/package.json @@ -18,7 +18,9 @@ "src" ], "scripts": { - "lint": "eslint ." + "lint": "eslint .", + "test": "vitest run", + "test:watch": "vitest" }, "peerDependencies": { "@manacore/shared-auth": "workspace:*", diff --git a/packages/shared-auth-ui/src/utils/guestWelcome.spec.ts b/packages/shared-auth-ui/src/utils/guestWelcome.spec.ts new file mode 100644 index 000000000..448a2b2cf --- /dev/null +++ b/packages/shared-auth-ui/src/utils/guestWelcome.spec.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + shouldShowGuestWelcome, + markGuestWelcomeSeen, + resetGuestWelcome, + resetAllGuestWelcome, +} from './guestWelcome'; + +// Mock localStorage +const store: Record = {}; +const localStorageMock = { + getItem: vi.fn((key: string) => store[key] ?? null), + setItem: vi.fn((key: string, value: string) => { + store[key] = value; + }), + removeItem: vi.fn((key: string) => { + delete store[key]; + }), + get length() { + return Object.keys(store).length; + }, + key: vi.fn((i: number) => Object.keys(store)[i] ?? null), + clear: vi.fn(() => { + for (const key of Object.keys(store)) delete store[key]; + }), +}; + +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock }); + +beforeEach(() => { + for (const key of Object.keys(store)) delete store[key]; + vi.clearAllMocks(); +}); + +describe('shouldShowGuestWelcome', () => { + it('returns true when not seen before', () => { + expect(shouldShowGuestWelcome('todo')).toBe(true); + }); + + it('returns false after marking as seen', () => { + markGuestWelcomeSeen('todo'); + expect(shouldShowGuestWelcome('todo')).toBe(false); + }); + + it('scopes to app ID', () => { + markGuestWelcomeSeen('todo'); + expect(shouldShowGuestWelcome('todo')).toBe(false); + expect(shouldShowGuestWelcome('chat')).toBe(true); + }); +}); + +describe('markGuestWelcomeSeen', () => { + it('writes to localStorage with correct key', () => { + markGuestWelcomeSeen('contacts'); + expect(localStorageMock.setItem).toHaveBeenCalledWith('guest-welcome-seen-contacts', 'true'); + }); +}); + +describe('resetGuestWelcome', () => { + it('removes the key for a specific app', () => { + markGuestWelcomeSeen('todo'); + expect(shouldShowGuestWelcome('todo')).toBe(false); + resetGuestWelcome('todo'); + expect(shouldShowGuestWelcome('todo')).toBe(true); + }); + + it('does not affect other apps', () => { + markGuestWelcomeSeen('todo'); + markGuestWelcomeSeen('chat'); + resetGuestWelcome('todo'); + expect(shouldShowGuestWelcome('chat')).toBe(false); + }); +}); + +describe('resetAllGuestWelcome', () => { + it('removes all guest-welcome keys', () => { + markGuestWelcomeSeen('todo'); + markGuestWelcomeSeen('chat'); + markGuestWelcomeSeen('calendar'); + resetAllGuestWelcome(); + expect(shouldShowGuestWelcome('todo')).toBe(true); + expect(shouldShowGuestWelcome('chat')).toBe(true); + expect(shouldShowGuestWelcome('calendar')).toBe(true); + }); + + it('does not remove unrelated keys', () => { + store['other-key'] = 'value'; + markGuestWelcomeSeen('todo'); + resetAllGuestWelcome(); + expect(store['other-key']).toBe('value'); + }); +}); diff --git a/packages/shared-auth-ui/src/utils/userAgent.spec.ts b/packages/shared-auth-ui/src/utils/userAgent.spec.ts new file mode 100644 index 000000000..2f81a875b --- /dev/null +++ b/packages/shared-auth-ui/src/utils/userAgent.spec.ts @@ -0,0 +1,97 @@ +import { describe, it, expect } from 'vitest'; +import { parseUserAgent, getDeviceType, formatUserAgent } from './userAgent'; + +describe('parseUserAgent', () => { + it('returns empty strings for null', () => { + expect(parseUserAgent(null)).toEqual({ browser: '', os: '' }); + }); + + it('returns empty strings for empty string', () => { + expect(parseUserAgent('')).toEqual({ browser: '', os: '' }); + }); + + it('detects Chrome on macOS', () => { + const ua = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Chrome', os: 'macOS' }); + }); + + it('detects Firefox on Windows', () => { + const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Firefox', os: 'Windows' }); + }); + + it('detects Safari on macOS', () => { + const ua = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Safari', os: 'macOS' }); + }); + + it('detects Edge on Windows', () => { + const ua = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Edge', os: 'Windows' }); + }); + + it('detects Chrome on Android', () => { + const ua = + 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Chrome', os: 'Android' }); + }); + + it('detects Safari on iOS', () => { + const ua = + 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Safari', os: 'iOS' }); + }); + + it('detects Chrome on Linux', () => { + const ua = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; + expect(parseUserAgent(ua)).toEqual({ browser: 'Chrome', os: 'Linux' }); + }); +}); + +describe('getDeviceType', () => { + it('returns desktop for null', () => { + expect(getDeviceType(null)).toBe('desktop'); + }); + + it('returns mobile for iPhone', () => { + const ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)'; + expect(getDeviceType(ua)).toBe('mobile'); + }); + + it('returns mobile for Android phone', () => { + const ua = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) Mobile Safari/537.36'; + expect(getDeviceType(ua)).toBe('mobile'); + }); + + it('returns tablet for iPad', () => { + const ua = 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X)'; + expect(getDeviceType(ua)).toBe('tablet'); + }); + + it('returns desktop for macOS Chrome', () => { + const ua = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0'; + expect(getDeviceType(ua)).toBe('desktop'); + }); +}); + +describe('formatUserAgent', () => { + it('returns empty string for null', () => { + expect(formatUserAgent(null)).toBe(''); + }); + + it('formats browser and OS with separator', () => { + const ua = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36'; + expect(formatUserAgent(ua)).toBe('Chrome \u00b7 macOS'); + }); + + it('formats browser only if OS unknown', () => { + const ua = 'Mozilla/5.0 Chrome/120.0.0.0'; + expect(formatUserAgent(ua)).toBe('Chrome'); + }); +}); diff --git a/packages/shared-auth-ui/src/utils/userAgent.ts b/packages/shared-auth-ui/src/utils/userAgent.ts index 3b8ddb395..3f7bd8792 100644 --- a/packages/shared-auth-ui/src/utils/userAgent.ts +++ b/packages/shared-auth-ui/src/utils/userAgent.ts @@ -13,11 +13,11 @@ export function parseUserAgent(ua: string | null): { browser: string; os: string else if (ua.includes('Safari/') && !ua.includes('Chrome/')) browser = 'Safari'; else if (ua.includes('Opera/') || ua.includes('OPR/')) browser = 'Opera'; - if (ua.includes('Windows')) os = 'Windows'; - else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) os = 'macOS'; - else if (ua.includes('Linux') && !ua.includes('Android')) os = 'Linux'; + if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS'; else if (ua.includes('Android')) os = 'Android'; - else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS'; + else if (ua.includes('Windows')) os = 'Windows'; + else if (ua.includes('Mac OS X') || ua.includes('Macintosh')) os = 'macOS'; + else if (ua.includes('Linux')) os = 'Linux'; return { browser, os }; } diff --git a/packages/shared-auth-ui/vitest.config.js b/packages/shared-auth-ui/vitest.config.js new file mode 100644 index 000000000..9968f94de --- /dev/null +++ b/packages/shared-auth-ui/vitest.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.{test,spec}.ts'], + environment: 'node', + globals: true, + clearMocks: true, + mockReset: true, + restoreMocks: true, + }, +}); diff --git a/packages/shared-auth/package.json b/packages/shared-auth/package.json index 3d21b59c2..c71d6d55b 100644 --- a/packages/shared-auth/package.json +++ b/packages/shared-auth/package.json @@ -12,6 +12,8 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", "type-check": "tsc --noEmit", "lint": "eslint ." }, diff --git a/packages/shared-auth/src/core/jwtUtils.spec.ts b/packages/shared-auth/src/core/jwtUtils.spec.ts new file mode 100644 index 000000000..92258ac2a --- /dev/null +++ b/packages/shared-auth/src/core/jwtUtils.spec.ts @@ -0,0 +1,209 @@ +import { describe, it, expect } from 'vitest'; +import { + decodeToken, + isTokenValidLocally, + isTokenExpired, + getUserFromToken, + getTokenExpirationTime, + getTimeUntilExpiration, + isB2BUser, + getB2BInfo, +} from './jwtUtils'; + +// Helper: create a fake JWT with given payload +function createToken(payload: Record): string { + const header = btoa(JSON.stringify({ alg: 'EdDSA', typ: 'JWT' })); + const body = btoa(JSON.stringify(payload)); + return `${header}.${body}.fakesignature`; +} + +describe('decodeToken', () => { + it('decodes a valid JWT payload', () => { + const token = createToken({ sub: 'user-1', email: 'test@example.com', exp: 9999999999 }); + const decoded = decodeToken(token); + expect(decoded).toMatchObject({ sub: 'user-1', email: 'test@example.com' }); + }); + + it('returns null for invalid token', () => { + expect(decodeToken('not-a-jwt')).toBeNull(); + }); + + it('returns null for empty string', () => { + expect(decodeToken('')).toBeNull(); + }); + + it('returns null for malformed base64', () => { + expect(decodeToken('a.!!!invalid!!!.c')).toBeNull(); + }); +}); + +describe('isTokenValidLocally', () => { + it('returns true for non-expired token', () => { + const futureExp = Math.floor(Date.now() / 1000) + 3600; + const token = createToken({ sub: 'u', exp: futureExp }); + expect(isTokenValidLocally(token)).toBe(true); + }); + + it('returns false for expired token', () => { + const pastExp = Math.floor(Date.now() / 1000) - 60; + const token = createToken({ sub: 'u', exp: pastExp }); + expect(isTokenValidLocally(token)).toBe(false); + }); + + it('returns false when within buffer', () => { + const almostExpired = Math.floor(Date.now() / 1000) + 5; + const token = createToken({ sub: 'u', exp: almostExpired }); + expect(isTokenValidLocally(token, 10)).toBe(false); + }); + + it('returns false for token without exp', () => { + const token = createToken({ sub: 'u' }); + expect(isTokenValidLocally(token)).toBe(false); + }); +}); + +describe('isTokenExpired', () => { + it('returns true for expired token', () => { + const pastExp = Math.floor(Date.now() / 1000) - 60; + const token = createToken({ sub: 'u', exp: pastExp }); + expect(isTokenExpired(token)).toBe(true); + }); + + it('returns false for valid token', () => { + const futureExp = Math.floor(Date.now() / 1000) + 3600; + const token = createToken({ sub: 'u', exp: futureExp }); + expect(isTokenExpired(token)).toBe(false); + }); +}); + +describe('getUserFromToken', () => { + it('extracts user data with tier', () => { + const token = createToken({ + sub: 'user-123', + email: 'test@mana.how', + role: 'admin', + tier: 'founder', + exp: 9999999999, + }); + const user = getUserFromToken(token); + expect(user).toEqual({ + id: 'user-123', + email: 'test@mana.how', + role: 'admin', + tier: 'founder', + }); + }); + + it('defaults tier to public when missing', () => { + const token = createToken({ sub: 'u', email: 'a@b.c', exp: 9999999999 }); + const user = getUserFromToken(token); + expect(user?.tier).toBe('public'); + }); + + it('defaults role to user when missing', () => { + const token = createToken({ sub: 'u', email: 'a@b.c', exp: 9999999999 }); + expect(getUserFromToken(token)?.role).toBe('user'); + }); + + it('falls back to user_metadata.email', () => { + const token = createToken({ + sub: 'u', + user_metadata: { email: 'meta@test.com' }, + exp: 9999999999, + }); + expect(getUserFromToken(token)?.email).toBe('meta@test.com'); + }); + + it('falls back to storedEmail', () => { + const token = createToken({ sub: 'u', exp: 9999999999 }); + expect(getUserFromToken(token, 'stored@test.com')?.email).toBe('stored@test.com'); + }); + + it('defaults email to user@example.com when all sources empty', () => { + const token = createToken({ sub: 'u', exp: 9999999999 }); + expect(getUserFromToken(token)?.email).toBe('user@example.com'); + }); + + it('returns null for invalid token', () => { + expect(getUserFromToken('garbage')).toBeNull(); + }); +}); + +describe('getTokenExpirationTime', () => { + it('returns exp in milliseconds', () => { + const exp = 1700000000; + const token = createToken({ sub: 'u', exp }); + expect(getTokenExpirationTime(token)).toBe(exp * 1000); + }); + + it('returns null without exp', () => { + const token = createToken({ sub: 'u' }); + expect(getTokenExpirationTime(token)).toBeNull(); + }); +}); + +describe('getTimeUntilExpiration', () => { + it('returns positive ms for future token', () => { + const futureExp = Math.floor(Date.now() / 1000) + 3600; + const token = createToken({ sub: 'u', exp: futureExp }); + const remaining = getTimeUntilExpiration(token); + expect(remaining).toBeGreaterThan(3500000); + expect(remaining).toBeLessThanOrEqual(3600000); + }); + + it('returns 0 for expired token', () => { + const pastExp = Math.floor(Date.now() / 1000) - 60; + const token = createToken({ sub: 'u', exp: pastExp }); + expect(getTimeUntilExpiration(token)).toBe(0); + }); +}); + +describe('isB2BUser', () => { + it('returns true for is_b2b: true', () => { + const token = createToken({ sub: 'u', is_b2b: true, exp: 9999999999 }); + expect(isB2BUser(token)).toBe(true); + }); + + it('returns true for is_b2b: "true"', () => { + const token = createToken({ sub: 'u', is_b2b: 'true', exp: 9999999999 }); + expect(isB2BUser(token)).toBe(true); + }); + + it('returns true for is_b2b: 1', () => { + const token = createToken({ sub: 'u', is_b2b: 1, exp: 9999999999 }); + expect(isB2BUser(token)).toBe(true); + }); + + it('returns false when not set', () => { + const token = createToken({ sub: 'u', exp: 9999999999 }); + expect(isB2BUser(token)).toBe(false); + }); +}); + +describe('getB2BInfo', () => { + it('extracts B2B settings', () => { + const token = createToken({ + sub: 'u', + app_settings: { + b2b: { + disableRevenueCat: true, + organizationId: 'org-1', + plan: 'enterprise', + role: 'admin', + }, + }, + exp: 9999999999, + }); + expect(getB2BInfo(token)).toEqual({ + disableRevenueCat: true, + organizationId: 'org-1', + plan: 'enterprise', + role: 'admin', + }); + }); + + it('returns null when no B2B settings', () => { + const token = createToken({ sub: 'u', exp: 9999999999 }); + expect(getB2BInfo(token)).toBeNull(); + }); +}); diff --git a/packages/shared-auth/vitest.config.js b/packages/shared-auth/vitest.config.js new file mode 100644 index 000000000..9968f94de --- /dev/null +++ b/packages/shared-auth/vitest.config.js @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.{test,spec}.ts'], + environment: 'node', + globals: true, + clearMocks: true, + mockReset: true, + restoreMocks: true, + }, +}); diff --git a/packages/shared-branding/src/mana-apps.spec.ts b/packages/shared-branding/src/mana-apps.spec.ts new file mode 100644 index 000000000..6ff090ded --- /dev/null +++ b/packages/shared-branding/src/mana-apps.spec.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest'; +import { + hasAppAccess, + getTierLevel, + getAccessibleManaApps, + getActiveManaApps, + getManaApp, + getManaAppsByStatus, + MANA_APPS, +} from './mana-apps'; + +describe('getTierLevel', () => { + it('returns correct levels for all tiers', () => { + expect(getTierLevel('guest')).toBe(0); + expect(getTierLevel('public')).toBe(1); + expect(getTierLevel('beta')).toBe(2); + expect(getTierLevel('alpha')).toBe(3); + expect(getTierLevel('founder')).toBe(4); + }); + + it('returns 0 for unknown tier', () => { + expect(getTierLevel('unknown')).toBe(0); + expect(getTierLevel('')).toBe(0); + }); +}); + +describe('hasAppAccess', () => { + it('founder can access everything', () => { + expect(hasAppAccess('founder', 'guest')).toBe(true); + expect(hasAppAccess('founder', 'public')).toBe(true); + expect(hasAppAccess('founder', 'beta')).toBe(true); + expect(hasAppAccess('founder', 'alpha')).toBe(true); + expect(hasAppAccess('founder', 'founder')).toBe(true); + }); + + it('public cannot access beta+', () => { + expect(hasAppAccess('public', 'public')).toBe(true); + expect(hasAppAccess('public', 'beta')).toBe(false); + expect(hasAppAccess('public', 'alpha')).toBe(false); + expect(hasAppAccess('public', 'founder')).toBe(false); + }); + + it('beta can access beta and below', () => { + expect(hasAppAccess('beta', 'guest')).toBe(true); + expect(hasAppAccess('beta', 'public')).toBe(true); + expect(hasAppAccess('beta', 'beta')).toBe(true); + expect(hasAppAccess('beta', 'alpha')).toBe(false); + expect(hasAppAccess('beta', 'founder')).toBe(false); + }); + + it('guest can only access guest tier', () => { + expect(hasAppAccess('guest', 'guest')).toBe(true); + expect(hasAppAccess('guest', 'public')).toBe(false); + }); + + it('unknown tier treated as guest (level 0)', () => { + expect(hasAppAccess('unknown', 'guest')).toBe(true); + expect(hasAppAccess('unknown', 'public')).toBe(false); + }); +}); + +describe('getAccessibleManaApps', () => { + it('founder sees all non-archived apps', () => { + const founderApps = getAccessibleManaApps('founder'); + const activeApps = getActiveManaApps(); + expect(founderApps.length).toBe(activeApps.length); + }); + + it('public sees fewer apps than founder', () => { + const publicApps = getAccessibleManaApps('public'); + const founderApps = getAccessibleManaApps('founder'); + expect(publicApps.length).toBeLessThanOrEqual(founderApps.length); + }); + + it('guest sees no apps (all require at least public)', () => { + const guestApps = getAccessibleManaApps('guest'); + // All apps require at least 'public' tier + expect(guestApps.length).toBe(0); + }); + + it('excludes archived apps', () => { + const apps = getAccessibleManaApps('founder'); + const archivedApps = apps.filter((a) => a.archived); + expect(archivedApps.length).toBe(0); + }); +}); + +describe('getManaApp', () => { + it('returns app by ID', () => { + const todo = getManaApp('todo'); + expect(todo).toBeDefined(); + expect(todo?.name).toBe('Todo'); + }); + + it('returns undefined for unknown ID', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(getManaApp('nonexistent' as any)).toBeUndefined(); + }); +}); + +describe('getManaAppsByStatus', () => { + it('filters by status', () => { + const betaApps = getManaAppsByStatus('beta'); + betaApps.forEach((app) => { + expect(app.status).toBe('beta'); + }); + }); +}); + +describe('MANA_APPS integrity', () => { + it('every app has a requiredTier', () => { + MANA_APPS.forEach((app) => { + expect(app.requiredTier).toBeDefined(); + expect(['guest', 'public', 'beta', 'alpha', 'founder']).toContain(app.requiredTier); + }); + }); + + it('every app has bilingual descriptions', () => { + MANA_APPS.forEach((app) => { + expect(app.description.de).toBeTruthy(); + expect(app.description.en).toBeTruthy(); + }); + }); +});