@avelonjs/conformance 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/package.json +50 -0
- package/src/fakes/ai.ts +113 -0
- package/src/fakes/cache.ts +153 -0
- package/src/fakes/database.ts +612 -0
- package/src/fakes/flags.ts +67 -0
- package/src/fakes/identity.ts +253 -0
- package/src/fakes/logs.ts +101 -0
- package/src/fakes/mail.ts +140 -0
- package/src/fakes/notifications.ts +73 -0
- package/src/fakes/payments.ts +188 -0
- package/src/fakes/queue.ts +205 -0
- package/src/fakes/ratelimit.ts +143 -0
- package/src/fakes/realtime.ts +96 -0
- package/src/fakes/search.ts +127 -0
- package/src/fakes/social.ts +83 -0
- package/src/fakes/storage.ts +125 -0
- package/src/fakes/tokens.ts +106 -0
- package/src/harness.ts +103 -0
- package/src/index.ts +21 -0
- package/src/suites/ai.ts +167 -0
- package/src/suites/cache.ts +174 -0
- package/src/suites/database.ts +853 -0
- package/src/suites/flags.ts +80 -0
- package/src/suites/identity.ts +293 -0
- package/src/suites/index.ts +16 -0
- package/src/suites/logs.ts +116 -0
- package/src/suites/mail.ts +160 -0
- package/src/suites/notifications.ts +119 -0
- package/src/suites/payments.ts +200 -0
- package/src/suites/queue.ts +238 -0
- package/src/suites/ratelimit.ts +134 -0
- package/src/suites/realtime.ts +123 -0
- package/src/suites/search.ts +115 -0
- package/src/suites/social.ts +129 -0
- package/src/suites/storage.ts +158 -0
- package/src/suites/tokens.ts +105 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type BroadcastRealtimeSurface,
|
|
4
|
+
type PresenceRealtimeSurface,
|
|
5
|
+
type RealtimeCapabilities,
|
|
6
|
+
type RealtimeDriver,
|
|
7
|
+
type RealtimeMessage,
|
|
8
|
+
} from '@avelonjs/core'
|
|
9
|
+
import { assertCapabilitySurface, type SuiteContext } from '../harness'
|
|
10
|
+
|
|
11
|
+
type AssayDriver = RealtimeDriver<RealtimeCapabilities> &
|
|
12
|
+
Partial<PresenceRealtimeSurface & BroadcastRealtimeSurface>
|
|
13
|
+
|
|
14
|
+
function assay<TDriver extends AssayDriver>(
|
|
15
|
+
context: SuiteContext<TDriver>,
|
|
16
|
+
name: string,
|
|
17
|
+
assertion: (driver: TDriver) => Promise<void> | void,
|
|
18
|
+
): void {
|
|
19
|
+
test(name, async () => {
|
|
20
|
+
const driver = await context.create()
|
|
21
|
+
try {
|
|
22
|
+
await assertion(driver)
|
|
23
|
+
} finally {
|
|
24
|
+
await context.cleanup?.(driver)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasBroadcast(driver: AssayDriver): driver is AssayDriver & BroadcastRealtimeSurface {
|
|
30
|
+
return driver.capabilities.broadcast && typeof driver.broadcast === 'function'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasPresence(driver: AssayDriver): driver is AssayDriver & PresenceRealtimeSurface {
|
|
34
|
+
return driver.capabilities.presence && typeof driver.joinPresence === 'function'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Registers the portable realtime contract conformance suite.
|
|
39
|
+
*
|
|
40
|
+
* The contract exposes no deterministic transport failure, so the suite does not invent one for an
|
|
41
|
+
* error-taxonomy assertion.
|
|
42
|
+
*
|
|
43
|
+
* @param context Fresh isolated realtime drivers and optional cleanup.
|
|
44
|
+
*/
|
|
45
|
+
export function realtimeSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
|
|
46
|
+
describe(`realtime conformance: ${context.name}`, () => {
|
|
47
|
+
assay(context, 'reports a present and stable resolved realtime instance', (driver) => {
|
|
48
|
+
const instance = driver.instance
|
|
49
|
+
expect(instance.length).toBeGreaterThan(0)
|
|
50
|
+
expect(driver.instance).toBe(instance)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
assay(context, 'enforces every realtime capability surface in both directions', (driver) => {
|
|
54
|
+
const surfaces: readonly [string, boolean, readonly string[]][] = [
|
|
55
|
+
['presence', driver.capabilities.presence, ['joinPresence', 'leavePresence', 'presence']],
|
|
56
|
+
['broadcast', driver.capabilities.broadcast, ['broadcast']],
|
|
57
|
+
]
|
|
58
|
+
for (const [capability, declared, methods] of surfaces) {
|
|
59
|
+
assertCapabilitySurface(driver, capability, declared, methods)
|
|
60
|
+
assertCapabilitySurface(
|
|
61
|
+
{ capabilities: { [capability]: false } },
|
|
62
|
+
capability,
|
|
63
|
+
false,
|
|
64
|
+
methods,
|
|
65
|
+
)
|
|
66
|
+
expect(() =>
|
|
67
|
+
assertCapabilitySurface(
|
|
68
|
+
{ capabilities: { [capability]: false }, [methods[0] ?? 'missing']: () => undefined },
|
|
69
|
+
capability,
|
|
70
|
+
false,
|
|
71
|
+
methods,
|
|
72
|
+
),
|
|
73
|
+
).toThrow()
|
|
74
|
+
expect(() =>
|
|
75
|
+
assertCapabilitySurface(
|
|
76
|
+
{ capabilities: { [capability]: true } },
|
|
77
|
+
capability,
|
|
78
|
+
true,
|
|
79
|
+
methods,
|
|
80
|
+
),
|
|
81
|
+
).toThrow()
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
assay(context, 'delivers messages only to the subscribed channel', async (driver) => {
|
|
86
|
+
if (!hasBroadcast(driver)) return
|
|
87
|
+
const received: RealtimeMessage<{ id: string }>[] = []
|
|
88
|
+
await driver.subscribe<{ id: string }>('orders', (message) => {
|
|
89
|
+
received.push(message)
|
|
90
|
+
})
|
|
91
|
+
await driver.broadcast('audit', 'created', { id: 'wrong-channel' })
|
|
92
|
+
await driver.broadcast('orders', 'created', { id: 'order-1' })
|
|
93
|
+
|
|
94
|
+
expect(received).toEqual([
|
|
95
|
+
{ channel: 'orders', event: 'created', payload: { id: 'order-1' } },
|
|
96
|
+
])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
assay(context, 'stops delivery after a subscription is unsubscribed', async (driver) => {
|
|
100
|
+
if (!hasBroadcast(driver)) return
|
|
101
|
+
const received: string[] = []
|
|
102
|
+
const subscription = await driver.subscribe<string>('reports', (message) => {
|
|
103
|
+
received.push(message.payload)
|
|
104
|
+
})
|
|
105
|
+
expect(subscription.channel).toBe('reports')
|
|
106
|
+
await driver.broadcast('reports', 'ready', 'first')
|
|
107
|
+
await subscription.unsubscribe()
|
|
108
|
+
await driver.broadcast('reports', 'ready', 'leaked')
|
|
109
|
+
|
|
110
|
+
expect(received).toEqual(['first'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
assay(context, 'joins, lists, and leaves channel presence when declared', async (driver) => {
|
|
114
|
+
if (!hasPresence(driver)) return
|
|
115
|
+
const member = { actorId: 'actor-1', status: 'online', nested: { tab: 2 } }
|
|
116
|
+
await driver.joinPresence('workspace', member)
|
|
117
|
+
expect(await driver.presence('workspace')).toEqual([member])
|
|
118
|
+
expect(await driver.presence('other-workspace')).toEqual([])
|
|
119
|
+
await driver.leavePresence('workspace')
|
|
120
|
+
expect(await driver.presence('workspace')).toEqual([])
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
Invalid,
|
|
4
|
+
type SearchCapabilities,
|
|
5
|
+
type SearchDocument,
|
|
6
|
+
type SearchDriver,
|
|
7
|
+
} from '@avelonjs/core'
|
|
8
|
+
import { captureFailure, type SuiteContext } from '../harness'
|
|
9
|
+
|
|
10
|
+
type AssayDriver = SearchDriver<SearchCapabilities>
|
|
11
|
+
|
|
12
|
+
function assay<TDriver extends AssayDriver>(
|
|
13
|
+
context: SuiteContext<TDriver>,
|
|
14
|
+
name: string,
|
|
15
|
+
assertion: (driver: TDriver) => Promise<void> | void,
|
|
16
|
+
): void {
|
|
17
|
+
test(name, async () => {
|
|
18
|
+
const driver = await context.create()
|
|
19
|
+
try {
|
|
20
|
+
await assertion(driver)
|
|
21
|
+
} finally {
|
|
22
|
+
await context.cleanup?.(driver)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
|
|
28
|
+
const error = await captureFailure(operation)
|
|
29
|
+
expect(error).toBeInstanceOf(Invalid)
|
|
30
|
+
if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
|
|
31
|
+
expect(error.code).toBe('INVALID')
|
|
32
|
+
return error
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const documents: readonly SearchDocument[] = [
|
|
36
|
+
{ id: 'guide-1', fields: { title: 'Avelon guide', category: 'docs', locale: 'en' } },
|
|
37
|
+
{ id: 'guide-2', fields: { title: 'Deployment guide', category: 'docs', locale: 'fr' } },
|
|
38
|
+
{ id: 'news-1', fields: { title: 'Avelon release', category: 'news', locale: 'en' } },
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Registers the portable search contract conformance suite.
|
|
43
|
+
*
|
|
44
|
+
* Facets aggregate the complete filtered match set before pagination, matching the response's
|
|
45
|
+
* `total`; counting only the current page would make facet navigation change between pages.
|
|
46
|
+
* Facet support narrows query arguments rather than adding a method, so declared and undeclared
|
|
47
|
+
* values are tested behaviorally instead of through `assertCapabilitySurface`.
|
|
48
|
+
*
|
|
49
|
+
* @param context Fresh isolated search drivers and optional cleanup.
|
|
50
|
+
*/
|
|
51
|
+
export function searchSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
|
|
52
|
+
describe(`search conformance: ${context.name}`, () => {
|
|
53
|
+
assay(context, 'reports a present and stable resolved search instance', (driver) => {
|
|
54
|
+
const instance = driver.instance
|
|
55
|
+
expect(instance.length).toBeGreaterThan(0)
|
|
56
|
+
expect(driver.instance).toBe(instance)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
assay(context, 'honors the declared facet capability in both directions', async (driver) => {
|
|
60
|
+
await driver.index('articles', documents)
|
|
61
|
+
if (driver.capabilities.facets) {
|
|
62
|
+
const result = await driver.query('articles', 'guide', { facets: ['category'] })
|
|
63
|
+
expect(result.facets).toEqual({ category: { docs: 2 } })
|
|
64
|
+
} else {
|
|
65
|
+
const error = await expectInvalid(() =>
|
|
66
|
+
driver.query('articles', 'guide', { facets: ['category'] }),
|
|
67
|
+
)
|
|
68
|
+
expect(error.metadata.fields?.facets).toBeDefined()
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
assay(context, 'indexes, filters, paginates, and replaces documents', async (driver) => {
|
|
73
|
+
await driver.index('articles', documents)
|
|
74
|
+
const firstPage = await driver.query('articles', 'Avelon', {
|
|
75
|
+
filters: { locale: 'en' },
|
|
76
|
+
limit: 1,
|
|
77
|
+
offset: 1,
|
|
78
|
+
})
|
|
79
|
+
expect(firstPage.total).toBe(2)
|
|
80
|
+
expect(firstPage.hits.map((hit) => hit.document.id)).toEqual(['news-1'])
|
|
81
|
+
|
|
82
|
+
await driver.index('articles', [
|
|
83
|
+
{ id: 'guide-1', fields: { title: 'Replaced document', category: 'reference' } },
|
|
84
|
+
])
|
|
85
|
+
const replaced = await driver.query('articles', 'replaced')
|
|
86
|
+
expect(replaced.total).toBe(1)
|
|
87
|
+
expect(replaced.hits[0]?.document.fields.category).toBe('reference')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
assay(context, 'removes documents so they stop matching', async (driver) => {
|
|
91
|
+
await driver.index('articles', documents)
|
|
92
|
+
expect((await driver.query('articles', 'deployment')).total).toBe(1)
|
|
93
|
+
await driver.remove('articles', ['guide-2'])
|
|
94
|
+
expect((await driver.query('articles', 'deployment')).total).toBe(0)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assay(context, 'counts facets from the actual filtered result set', async (driver) => {
|
|
98
|
+
if (!driver.capabilities.facets) return
|
|
99
|
+
await driver.index('articles', documents)
|
|
100
|
+
const result = await driver.query('articles', 'Avelon', {
|
|
101
|
+
filters: { locale: 'en' },
|
|
102
|
+
facets: ['category', 'locale'],
|
|
103
|
+
limit: 1,
|
|
104
|
+
})
|
|
105
|
+
expect(result.hits).toHaveLength(1)
|
|
106
|
+
expect(result.total).toBe(2)
|
|
107
|
+
expect(result.facets).toEqual({ category: { docs: 1, news: 1 }, locale: { en: 2 } })
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
assay(context, 'normalizes invalid pagination options to Invalid', async (driver) => {
|
|
111
|
+
const error = await expectInvalid(() => driver.query('articles', '', { limit: -1 }))
|
|
112
|
+
expect(Object.keys(error.metadata.fields ?? {})).toContain('limit')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Invalid, Unauthenticated, type SocialCapabilities, type SocialDriver } from '@avelonjs/core'
|
|
3
|
+
import { captureFailure, type SuiteContext } from '../harness'
|
|
4
|
+
|
|
5
|
+
type AssayDriver = SocialDriver<SocialCapabilities<string>, unknown, unknown>
|
|
6
|
+
|
|
7
|
+
function assay<TDriver extends AssayDriver>(
|
|
8
|
+
context: SuiteContext<TDriver>,
|
|
9
|
+
name: string,
|
|
10
|
+
assertion: (driver: TDriver) => Promise<void> | void,
|
|
11
|
+
): void {
|
|
12
|
+
test(name, async () => {
|
|
13
|
+
const driver = await context.create()
|
|
14
|
+
try {
|
|
15
|
+
await assertion(driver)
|
|
16
|
+
} finally {
|
|
17
|
+
await context.cleanup?.(driver)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function expectUnauthenticated(operation: () => Promise<unknown>): Promise<Unauthenticated> {
|
|
23
|
+
const error = await captureFailure(operation)
|
|
24
|
+
expect(error).toBeInstanceOf(Unauthenticated)
|
|
25
|
+
if (!(error instanceof Unauthenticated)) {
|
|
26
|
+
throw new Error('Expected Unauthenticated after taxonomy assertion.')
|
|
27
|
+
}
|
|
28
|
+
expect(error.code).toBe('UNAUTHENTICATED')
|
|
29
|
+
return error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function expectInvalid(operation: () => Promise<unknown>): Promise<Invalid> {
|
|
33
|
+
const error = await captureFailure(operation)
|
|
34
|
+
expect(error).toBeInstanceOf(Invalid)
|
|
35
|
+
if (!(error instanceof Invalid)) throw new Error('Expected Invalid after taxonomy assertion.')
|
|
36
|
+
expect(error.code).toBe('INVALID')
|
|
37
|
+
return error
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function firstProvider(driver: AssayDriver): string | undefined {
|
|
41
|
+
return driver.capabilities.providers[0]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Registers the portable social authorization contract conformance suite.
|
|
46
|
+
*
|
|
47
|
+
* State failures and provider-declined callbacks are pinned to `Unauthenticated` because no trusted
|
|
48
|
+
* identity was established. An undeclared provider or missing authorization code is pinned to
|
|
49
|
+
* `Invalid` because the framework supplied malformed input.
|
|
50
|
+
*
|
|
51
|
+
* @param context Fresh driver instances and optional live-service cleanup.
|
|
52
|
+
*/
|
|
53
|
+
export function socialSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
|
|
54
|
+
describe(`social conformance: ${context.name}`, () => {
|
|
55
|
+
assay(context, 'completes the redirect and callback round trip', async (driver) => {
|
|
56
|
+
const provider = firstProvider(driver)
|
|
57
|
+
if (!provider) return
|
|
58
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
59
|
+
const state = 'expected-state'
|
|
60
|
+
const redirect = await driver.redirect(provider, callbackUrl, state)
|
|
61
|
+
expect(redirect).toContain(encodeURIComponent(callbackUrl))
|
|
62
|
+
expect(redirect).toContain(state)
|
|
63
|
+
|
|
64
|
+
const identity = await driver.callback(provider, { code: 'valid-code', state }, callbackUrl)
|
|
65
|
+
expect(identity.provider).toBe(provider)
|
|
66
|
+
expect(identity.subject.length).toBeGreaterThan(0)
|
|
67
|
+
expect(identity.profile).toBeDefined()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
assay(context, 'generates and verifies state when the caller omits it', async (driver) => {
|
|
71
|
+
const provider = firstProvider(driver)
|
|
72
|
+
if (!provider) return
|
|
73
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
74
|
+
const redirect = new URL(await driver.redirect(provider, callbackUrl))
|
|
75
|
+
const state = redirect.searchParams.get('state')
|
|
76
|
+
expect(state).not.toBeNull()
|
|
77
|
+
if (!state) throw new Error('Expected generated state after assertion.')
|
|
78
|
+
await expect(
|
|
79
|
+
driver.callback(provider, { code: 'valid-code', state }, callbackUrl),
|
|
80
|
+
).resolves.toMatchObject({ provider })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
assay(context, 'rejects a mismatched OAuth state as Unauthenticated', async (driver) => {
|
|
84
|
+
const provider = firstProvider(driver)
|
|
85
|
+
if (!provider) return
|
|
86
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
87
|
+
await driver.redirect(provider, callbackUrl, 'expected-state')
|
|
88
|
+
await expectUnauthenticated(() =>
|
|
89
|
+
driver.callback(provider, { code: 'valid-code', state: 'attacker-state' }, callbackUrl),
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
assay(context, 'rejects a missing OAuth state as Unauthenticated', async (driver) => {
|
|
94
|
+
const provider = firstProvider(driver)
|
|
95
|
+
if (!provider) return
|
|
96
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
97
|
+
await driver.redirect(provider, callbackUrl, 'expected-state')
|
|
98
|
+
await expectUnauthenticated(() =>
|
|
99
|
+
driver.callback(provider, { code: 'valid-code' }, callbackUrl),
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
assay(context, 'rejects undeclared providers as Invalid', async (driver) => {
|
|
104
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
105
|
+
await expectInvalid(() => driver.redirect('undeclared-provider', callbackUrl, 'state'))
|
|
106
|
+
await expectInvalid(() =>
|
|
107
|
+
driver.callback('undeclared-provider', { code: 'valid-code', state: 'state' }, callbackUrl),
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
assay(context, 'normalizes a provider error callback to Unauthenticated', async (driver) => {
|
|
112
|
+
const provider = firstProvider(driver)
|
|
113
|
+
if (!provider) return
|
|
114
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
115
|
+
await driver.redirect(provider, callbackUrl, 'expected-state')
|
|
116
|
+
await expectUnauthenticated(() =>
|
|
117
|
+
driver.callback(provider, { error: 'access_denied', state: 'expected-state' }, callbackUrl),
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
assay(context, 'rejects a callback without a code as Invalid', async (driver) => {
|
|
122
|
+
const provider = firstProvider(driver)
|
|
123
|
+
if (!provider) return
|
|
124
|
+
const callbackUrl = 'https://app.example.test/social/callback'
|
|
125
|
+
await driver.redirect(provider, callbackUrl, 'expected-state')
|
|
126
|
+
await expectInvalid(() => driver.callback(provider, { state: 'expected-state' }, callbackUrl))
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
NotFound,
|
|
4
|
+
type SignedUrlStorageSurface,
|
|
5
|
+
type StorageCapabilities,
|
|
6
|
+
type StorageDriver,
|
|
7
|
+
type StorageTransform,
|
|
8
|
+
type TransformStorageSurface,
|
|
9
|
+
} from '@avelonjs/core'
|
|
10
|
+
import { assertCapabilitySurface, captureFailure, type SuiteContext } from '../harness'
|
|
11
|
+
|
|
12
|
+
type AssayDriver = StorageDriver<StorageCapabilities> &
|
|
13
|
+
Partial<SignedUrlStorageSurface & TransformStorageSurface<StorageTransform>>
|
|
14
|
+
|
|
15
|
+
function assay<TDriver extends AssayDriver>(
|
|
16
|
+
context: SuiteContext<TDriver>,
|
|
17
|
+
name: string,
|
|
18
|
+
assertion: (driver: TDriver) => Promise<void> | void,
|
|
19
|
+
): void {
|
|
20
|
+
test(name, async () => {
|
|
21
|
+
const driver = await context.create()
|
|
22
|
+
try {
|
|
23
|
+
await assertion(driver)
|
|
24
|
+
} finally {
|
|
25
|
+
await context.cleanup?.(driver)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasSignedUrls(driver: AssayDriver): driver is AssayDriver & SignedUrlStorageSurface {
|
|
31
|
+
return driver.capabilities.signedUrls && typeof driver.signedUrl === 'function'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function expectNotFound(operation: () => Promise<unknown>): Promise<NotFound> {
|
|
35
|
+
const error = await captureFailure(operation)
|
|
36
|
+
expect(error).toBeInstanceOf(NotFound)
|
|
37
|
+
if (!(error instanceof NotFound)) throw new Error('Expected NotFound after taxonomy assertion.')
|
|
38
|
+
expect(error.code).toBe('NOT_FOUND')
|
|
39
|
+
return error
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registers the portable storage contract conformance suite.
|
|
44
|
+
*
|
|
45
|
+
* The context supplies one already-resolved disk. Named-disk resolution and unknown-name rejection
|
|
46
|
+
* belong to configuration and the storage facade; this suite verifies only the driver's stable
|
|
47
|
+
* instance identity. Signed upload URLs and object listing are deliberately outside the v1 contract
|
|
48
|
+
* under D28 and are not tested here.
|
|
49
|
+
*
|
|
50
|
+
* @param context Fresh isolated storage drivers and optional cleanup.
|
|
51
|
+
*/
|
|
52
|
+
export function storageSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
|
|
53
|
+
describe(`storage conformance: ${context.name}`, () => {
|
|
54
|
+
assay(context, 'reports a present and stable resolved disk instance', (driver) => {
|
|
55
|
+
const instance = driver.instance
|
|
56
|
+
expect(instance.length).toBeGreaterThan(0)
|
|
57
|
+
expect(driver.instance).toBe(instance)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
assay(context, 'enforces every storage capability surface in both directions', (driver) => {
|
|
61
|
+
const surfaces: readonly [string, boolean, readonly string[]][] = [
|
|
62
|
+
['signedUrls', driver.capabilities.signedUrls, ['signedUrl']],
|
|
63
|
+
['transforms', driver.capabilities.transforms.length > 0, ['transform']],
|
|
64
|
+
]
|
|
65
|
+
for (const [capability, declared, methods] of surfaces) {
|
|
66
|
+
assertCapabilitySurface(driver, capability, declared, methods)
|
|
67
|
+
const unavailable = { capabilities: { [capability]: false } }
|
|
68
|
+
assertCapabilitySurface(unavailable, capability, false, methods)
|
|
69
|
+
expect(() =>
|
|
70
|
+
assertCapabilitySurface(
|
|
71
|
+
{ ...unavailable, [methods[0] ?? 'missing']: () => undefined },
|
|
72
|
+
capability,
|
|
73
|
+
false,
|
|
74
|
+
methods,
|
|
75
|
+
),
|
|
76
|
+
).toThrow()
|
|
77
|
+
expect(() =>
|
|
78
|
+
assertCapabilitySurface(
|
|
79
|
+
{ capabilities: { [capability]: true } },
|
|
80
|
+
capability,
|
|
81
|
+
true,
|
|
82
|
+
methods,
|
|
83
|
+
),
|
|
84
|
+
).toThrow()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
assay(context, 'round-trips arbitrary bytes without text coercion', async (driver) => {
|
|
89
|
+
const contents = new Uint8Array([0, 255, 1, 0, 128, 13, 10])
|
|
90
|
+
const stored = await driver.put('binary/nulls.bin', contents, {
|
|
91
|
+
contentType: 'application/octet-stream',
|
|
92
|
+
})
|
|
93
|
+
expect(stored).toEqual({
|
|
94
|
+
path: 'binary/nulls.bin',
|
|
95
|
+
size: contents.byteLength,
|
|
96
|
+
contentType: 'application/octet-stream',
|
|
97
|
+
})
|
|
98
|
+
expect(await driver.get('binary/nulls.bin')).toEqual(contents)
|
|
99
|
+
|
|
100
|
+
contents.fill(42)
|
|
101
|
+
expect(await driver.get('binary/nulls.bin')).toEqual(
|
|
102
|
+
new Uint8Array([0, 255, 1, 0, 128, 13, 10]),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
assay(
|
|
107
|
+
context,
|
|
108
|
+
'stores async byte streams without changing chunk boundaries or bytes',
|
|
109
|
+
async (driver) => {
|
|
110
|
+
async function* chunks(): AsyncIterable<Uint8Array> {
|
|
111
|
+
yield new Uint8Array([0, 1])
|
|
112
|
+
yield new Uint8Array([255, 0, 2])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await driver.put('binary/stream.bin', chunks())
|
|
116
|
+
expect(await driver.get('binary/stream.bin')).toEqual(new Uint8Array([0, 1, 255, 0, 2]))
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
assay(context, 'reports existence and deletes idempotently', async (driver) => {
|
|
121
|
+
expect(await driver.exists('documents/report.bin')).toBe(false)
|
|
122
|
+
await driver.put('documents/report.bin', new Uint8Array([1, 2, 3]))
|
|
123
|
+
expect(await driver.exists('documents/report.bin')).toBe(true)
|
|
124
|
+
await driver.delete('documents/report.bin')
|
|
125
|
+
expect(await driver.exists('documents/report.bin')).toBe(false)
|
|
126
|
+
await expect(driver.delete('documents/report.bin')).resolves.toBeUndefined()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
assay(context, 'normalizes missing-object reads to NotFound', async (driver) => {
|
|
130
|
+
const error = await expectNotFound(() => driver.get('missing/object.bin'))
|
|
131
|
+
expect(error.metadata).toEqual({
|
|
132
|
+
resource: 'storage-object',
|
|
133
|
+
identifier: 'missing/object.bin',
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
assay(context, 'serves signed read URLs only until their declared expiry', async (driver) => {
|
|
138
|
+
if (!hasSignedUrls(driver)) return
|
|
139
|
+
const contents = new Uint8Array([0, 9, 255, 0])
|
|
140
|
+
await driver.put('signed/read.bin', contents, { contentType: 'application/octet-stream' })
|
|
141
|
+
const url = await driver.signedUrl('signed/read.bin', 1)
|
|
142
|
+
|
|
143
|
+
const beforeExpiry = await fetch(url)
|
|
144
|
+
expect(beforeExpiry.ok).toBe(true)
|
|
145
|
+
expect(new Uint8Array(await beforeExpiry.arrayBuffer())).toEqual(contents)
|
|
146
|
+
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 1_200))
|
|
148
|
+
let expired = false
|
|
149
|
+
try {
|
|
150
|
+
const afterExpiry = await fetch(url)
|
|
151
|
+
expired = !afterExpiry.ok
|
|
152
|
+
} catch (_error: unknown) {
|
|
153
|
+
expired = true
|
|
154
|
+
}
|
|
155
|
+
expect(expired).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Unauthenticated, type TokenCapabilities, type TokenDriver } from '@avelonjs/core'
|
|
3
|
+
import { captureFailure, type SuiteContext } from '../harness'
|
|
4
|
+
|
|
5
|
+
type AssayDriver = TokenDriver<TokenCapabilities, unknown>
|
|
6
|
+
|
|
7
|
+
function assay<TDriver extends AssayDriver>(
|
|
8
|
+
context: SuiteContext<TDriver>,
|
|
9
|
+
name: string,
|
|
10
|
+
assertion: (driver: TDriver) => Promise<void> | void,
|
|
11
|
+
): void {
|
|
12
|
+
test(name, async () => {
|
|
13
|
+
const driver = await context.create()
|
|
14
|
+
try {
|
|
15
|
+
await assertion(driver)
|
|
16
|
+
} finally {
|
|
17
|
+
await context.cleanup?.(driver)
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function expectUnauthenticated(operation: () => Promise<unknown>): Promise<Unauthenticated> {
|
|
23
|
+
const error = await captureFailure(operation)
|
|
24
|
+
expect(error).toBeInstanceOf(Unauthenticated)
|
|
25
|
+
if (!(error instanceof Unauthenticated)) {
|
|
26
|
+
throw new Error('Expected Unauthenticated after taxonomy assertion.')
|
|
27
|
+
}
|
|
28
|
+
expect(error.code).toBe('UNAUTHENTICATED')
|
|
29
|
+
return error
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Registers the portable API-token contract conformance suite.
|
|
34
|
+
*
|
|
35
|
+
* Ability enforcement is pinned as exact scope preservation on `verify()`: the contract has no
|
|
36
|
+
* ability-check operation, so authorization layers enforce the returned set. Expiration assertions
|
|
37
|
+
* run only when the driver declares expiration support.
|
|
38
|
+
*
|
|
39
|
+
* @param context Fresh driver instances and optional live-service cleanup.
|
|
40
|
+
*/
|
|
41
|
+
export function tokensSuite<TDriver extends AssayDriver>(context: SuiteContext<TDriver>): void {
|
|
42
|
+
describe(`tokens conformance: ${context.name}`, () => {
|
|
43
|
+
assay(
|
|
44
|
+
context,
|
|
45
|
+
'issues and lists token metadata without recovering plaintext',
|
|
46
|
+
async (driver) => {
|
|
47
|
+
const issued = await driver.issue('deployment')
|
|
48
|
+
expect(issued.id.length).toBeGreaterThan(0)
|
|
49
|
+
expect(issued.subject.length).toBeGreaterThan(0)
|
|
50
|
+
expect(issued.name).toBe('deployment')
|
|
51
|
+
expect(issued.plainText.length).toBeGreaterThan(0)
|
|
52
|
+
expect(issued.createdAt).toBeInstanceOf(Date)
|
|
53
|
+
|
|
54
|
+
const listed = await driver.list()
|
|
55
|
+
expect(listed).toHaveLength(1)
|
|
56
|
+
expect(listed[0]).toEqual({
|
|
57
|
+
id: issued.id,
|
|
58
|
+
subject: issued.subject,
|
|
59
|
+
name: issued.name,
|
|
60
|
+
abilities: issued.abilities,
|
|
61
|
+
createdAt: issued.createdAt,
|
|
62
|
+
expiresAt: issued.expiresAt,
|
|
63
|
+
})
|
|
64
|
+
const record = listed[0]
|
|
65
|
+
expect(record).toBeDefined()
|
|
66
|
+
if (!record) throw new Error('Expected listed token after assertion.')
|
|
67
|
+
expect(Reflect.has(record, 'plainText')).toBe(false)
|
|
68
|
+
expect(JSON.stringify(listed)).not.toContain(issued.plainText)
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
assay(
|
|
73
|
+
context,
|
|
74
|
+
'verifies the token subject and exact declared ability scope',
|
|
75
|
+
async (driver) => {
|
|
76
|
+
if (!driver.capabilities.abilities) return
|
|
77
|
+
const issued = await driver.issue('scoped', { abilities: ['records:read'] })
|
|
78
|
+
const verified = await driver.verify(issued.plainText)
|
|
79
|
+
expect(verified.subject).toBe(issued.subject)
|
|
80
|
+
// Exact preservation prevents a driver from silently broadening a restricted credential.
|
|
81
|
+
expect(verified.abilities).toEqual(['records:read'])
|
|
82
|
+
expect(verified.abilities).not.toContain('records:write')
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
assay(context, 'rejects an unknown token as Unauthenticated', async (driver) => {
|
|
87
|
+
await expectUnauthenticated(() => driver.verify('unknown-token'))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
assay(context, 'revokes an issued token and rejects it as Unauthenticated', async (driver) => {
|
|
91
|
+
const issued = await driver.issue('revoked')
|
|
92
|
+
expect(await driver.verify(issued.plainText)).toMatchObject({ id: issued.id })
|
|
93
|
+
await driver.revoke(issued.id)
|
|
94
|
+
await expectUnauthenticated(() => driver.verify(issued.plainText))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
assay(context, 'rejects an expired token as Unauthenticated when declared', async (driver) => {
|
|
98
|
+
if (!driver.capabilities.expiration) return
|
|
99
|
+
const expiresAt = new Date(Date.now() - 1000)
|
|
100
|
+
const issued = await driver.issue('expired', { expiresAt })
|
|
101
|
+
expect(issued.expiresAt).toEqual(expiresAt)
|
|
102
|
+
await expectUnauthenticated(() => driver.verify(issued.plainText))
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
}
|