Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion lib/api/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2062,7 +2062,6 @@ export class MockAccessApi implements AccessApi {
}

public analytics: import('./types').AnalyticsDataSource = {
public analytics: any = {
getMembershipTrend: async (_signal?: AbortSignal) => {
await initPromise;
return getMemberGrowth();
Expand Down
141 changes: 128 additions & 13 deletions lib/api/analytics/mock.test.ts → test/mock-analytics.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,141 @@
/**
* lib/api/analytics/mock.test.ts
* test/mock-analytics.test.ts
*
* Tests for analytics mock data type safety and structure.
* Tests for the public analytics mock API and its fixture invariants.
*
* Acceptance criteria:
* - MOCK_ANALYTICS_SUMMARY has explicit resourceAccess field
* - resourceAccess field is not null/undefined
* - Type checking: resource access matches AnalyticsSummary type
* - Accessor functions return same data as direct field access
* - Accessor functions have proper return types (not any/unknown)
* - The mock compiles and satisfies AnalyticsSummary type
* Testing approach:
* Public API tests intentionally use literal expectations instead of deriving
* expected values from MOCK_ANALYTICS_SUMMARY. The remaining tests protect the
* internal fixture invariants that were covered before this file moved into the
* repository's executable test directory.
*/

import { test } from 'node:test'
import './setup-env'
import { describe, test } from 'node:test'
import * as assert from 'node:assert/strict'
import { MockAccessApi } from '../lib/api/mock'
import {
MOCK_ANALYTICS_SUMMARY,
getResourceAccess,
getMemberGrowth,
getMockAnalyticsSummary,
} from './mock'
import type { AnalyticsSummary, ResourceAccessCount } from './types'
} from '../lib/api/analytics/mock'
import type {
AnalyticsSummary,
ResourceAccessCount,
} from '../lib/api/analytics/types'

const ADMIN_ADDRESS = '0x0000000000000000000000000000000000000001'

function createAdminApi(): MockAccessApi {
return new MockAccessApi(ADMIN_ADDRESS, 'guildpass-demo')
}

// -- Public MockAccessApi analytics contract ---------------------------------

describe('MockAccessApi.analytics public accessors', () => {
test('exposes exactly the analytics accessors declared by the public contract', () => {
assert.deepStrictEqual(
Object.keys(createAdminApi().analytics).sort(),
['getAccessAttempts', 'getMembershipTrend', 'getRoleDistribution'],
)
})

test('getMembershipTrend returns the public member-growth shape', async () => {
const trend = await createAdminApi().analytics.getMembershipTrend()

assert.equal(trend.length, 30)
trend.forEach((point, index) => {
assert.deepStrictEqual(
Object.keys(point).sort(),
['date', 'newMembers', 'totalMembers'],
`memberGrowth[${index}] must keep the public response shape`,
)
assert.match(point.date, /^\d{4}-\d{2}-\d{2}$/)
assert.ok(Number.isInteger(point.newMembers))
assert.ok(point.newMembers >= 0)
assert.ok(Number.isInteger(point.totalMembers))
assert.ok(point.totalMembers >= 0)
})

assert.equal(trend[0].totalMembers, 80 + trend[0].newMembers)
trend.slice(1).forEach((point, index) => {
const previous = trend[index]
const previousDate = Date.parse(`${previous.date}T00:00:00Z`)
const currentDate = Date.parse(`${point.date}T00:00:00Z`)

assert.equal(currentDate - previousDate, 24 * 60 * 60 * 1000)
assert.equal(point.totalMembers - previous.totalMembers, point.newMembers)
})
})

test('getRoleDistribution returns every role with valid seeded counts', async () => {
const distribution = await createAdminApi().analytics.getRoleDistribution()

distribution.forEach((entry, index) => {
assert.deepStrictEqual(
Object.keys(entry).sort(),
['count', 'role'],
`roleDistribution[${index}] must keep the public response shape`,
)
assert.ok(Number.isInteger(entry.count))
assert.ok(entry.count >= 0)
})
assert.deepStrictEqual(
[...distribution].sort((left, right) => left.role.localeCompare(right.role)),
[
{ role: 'admin', count: 1 },
{ role: 'member', count: 49001 },
{ role: 'moderator', count: 1062 },
],
)
})

test('getAccessAttempts exposes resourceAccess through the public API', async () => {
const accessAttempts = await createAdminApi().analytics.getAccessAttempts()

assert.equal(accessAttempts.length, 3)
accessAttempts.forEach((entry, index) => {
assert.deepStrictEqual(
Object.keys(entry).sort(),
['accessCount', 'deniedCount', 'resourceId', 'resourceTitle'],
`resourceAccess[${index}] must keep the public response shape`,
)
assert.ok(Number.isInteger(entry.accessCount))
assert.ok(entry.accessCount >= 0)
assert.ok(Number.isInteger(entry.deniedCount))
assert.ok(entry.deniedCount >= 0)
})

const expectedEntries = [
{
resourceId: 'alpha',
resourceTitle: 'Alpha Docs',
accessCount: 312,
deniedCount: 47,
},
{
resourceId: 'pro-reports',
resourceTitle: 'Pro Reports',
accessCount: 189,
deniedCount: 103,
},
{
resourceId: 'mem-updates',
resourceTitle: 'Member Updates',
accessCount: 541,
deniedCount: 12,
},
]

expectedEntries.forEach((expected) => {
assert.deepStrictEqual(
accessAttempts.find((entry) => entry.resourceId === expected.resourceId),
expected,
)
})
})
})

// ── Mock structure tests ─────────────────────────────────────────────────────

Expand Down Expand Up @@ -193,7 +308,7 @@ test('getMockAnalyticsSummary() returns complete AnalyticsSummary object', () =>
)
})

test('getMockAnalyticsSummary() returns same object as MOCK_ANALYTICS_SUMMARY', () => {
test('getMockAnalyticsSummary() returns the same data as MOCK_ANALYTICS_SUMMARY', () => {
const result = getMockAnalyticsSummary()
const direct = MOCK_ANALYTICS_SUMMARY

Expand Down