Skip to content

Commit

Permalink
Add tests for document client
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulLeCam committed Jun 12, 2024
1 parent deae0b9 commit 998cb59
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 24 deletions.
5 changes: 3 additions & 2 deletions packages/document-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
"@ceramic-sdk/document-protocol": "workspace:^",
"@ceramic-sdk/events": "workspace:^",
"@didtools/codecs": "^3.0.0",
"codeco": "^1.2.3",
"fast-json-patch": "^3.1.1"
},
"devDependencies": {
"@ceramic-sdk/identifiers": "workspace:^",
"dids": "^5.0.2"
"@ceramic-sdk/key-did": "workspace:^",
"dids": "^5.0.2",
"uint8arrays": "^5.1.0"
},
"jest": {
"extensionsToTreatAsEsm": [".ts"],
Expand Down
24 changes: 13 additions & 11 deletions packages/document-client/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,22 @@ import {
} from '@ceramic-sdk/events'
import type { CommitID, StreamID } from '@ceramic-sdk/identifiers'
import type { DIDString } from '@didtools/codecs'
import { decode } from 'codeco'
import type { DID } from 'dids'

import type { UnknowContent } from './types.js'
import type { UnknownContent } from './types.js'
import { createInitHeader, getPatchOperations } from './utils.js'

export type CreateInitEventParams<T extends UnknowContent = UnknowContent> = {
export type CreateInitEventParams<T extends UnknownContent = UnknownContent> = {
content: T
controller: DID
model: StreamID
context?: StreamID
shouldIndex?: boolean
}

export async function createInitEvent<T extends UnknowContent = UnknowContent>(
params: CreateInitEventParams<T>,
): Promise<SignedEvent> {
export async function createInitEvent<
T extends UnknownContent = UnknownContent,
>(params: CreateInitEventParams<T>): Promise<SignedEvent> {
const { content, controller, ...headerParams } = params
assertValidContentLength(content)
const header = createInitHeader({
Expand Down Expand Up @@ -69,10 +68,13 @@ export function createDataEventPayload(
if (header != null) {
payload.header = header
}
return decode(DocumentDataEventPayload, payload)
if (!DocumentDataEventPayload.is(payload)) {
throw new Error('Invalid payload')
}
return payload
}

export type CreateDataEventParams<T extends UnknowContent = UnknowContent> = {
export type CreateDataEventParams<T extends UnknownContent = UnknownContent> = {
controller: DID
currentID: CommitID
content?: T
Expand All @@ -81,9 +83,9 @@ export type CreateDataEventParams<T extends UnknowContent = UnknowContent> = {
shouldIndex?: boolean
}

export async function createDataEvent<T extends UnknowContent = UnknowContent>(
params: CreateDataEventParams<T>,
): Promise<SignedEvent> {
export async function createDataEvent<
T extends UnknownContent = UnknownContent,
>(params: CreateDataEventParams<T>): Promise<SignedEvent> {
const operations = getPatchOperations(params.currentContent, params.content)
// Header must only be provided if there are values
// CBOR encoding doesn't support undefined values
Expand Down
7 changes: 4 additions & 3 deletions packages/document-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export {
CreateDataEventParams,
CreateInitEventParams,
type CreateDataEventParams,
type CreateInitEventParams,
createDataEvent,
createDataEventPayload,
createInitEvent,
getDeterministicInitEvent,
} from './events.js'
export type { UnknowContent } from './types.js'
export type { UnknownContent } from './types.js'
export { getPatchOperations } from './utils.js'
2 changes: 1 addition & 1 deletion packages/document-client/src/types.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type UnknowContent = Record<string, unknown>
export type UnknownContent = Record<string, unknown>
10 changes: 6 additions & 4 deletions packages/document-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import {
} from '@ceramic-sdk/document-protocol'
import type { StreamID } from '@ceramic-sdk/identifiers'
import { type DIDString, asDIDString } from '@didtools/codecs'
import { decode } from 'codeco'
import jsonpatch from 'fast-json-patch'

import type { UnknowContent } from './types.js'
import type { UnknownContent } from './types.js'

export function randomBytes(length: number): Uint8Array {
const bytes = new Uint8Array(length)
Expand Down Expand Up @@ -50,10 +49,13 @@ export function createInitHeader(
}

// Validate header before returning
return decode(DocumentInitEventHeader, header)
if (!DocumentInitEventHeader.is(header)) {
throw new Error('Invalid header')
}
return header
}

export function getPatchOperations<T extends UnknowContent = UnknowContent>(
export function getPatchOperations<T extends UnknownContent = UnknownContent>(
fromContent?: T,
toContent?: T,
): Array<JSONPatchOperation> {
Expand Down
115 changes: 115 additions & 0 deletions packages/document-client/test/lib.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
DocumentDataEventPayload,
DocumentInitEventPayload,
} from '@ceramic-sdk/document-protocol'
import { assertSignedEvent, getSignedEventPayload } from '@ceramic-sdk/events'
import { CommitID, randomCID, randomStreamID } from '@ceramic-sdk/identifiers'
import { getAuthenticatedDID } from '@ceramic-sdk/key-did'

import {
createDataEvent,
createInitEvent,
getDeterministicInitEvent,
} from '../src/index.js'

const authenticatedDID = await getAuthenticatedDID(new Uint8Array(32))

describe('getDeterministicInitEvent()', () => {
test('returns the deterministic event payload without unique value by default', () => {
const model = randomStreamID()
const event = getDeterministicInitEvent(model, 'did:key:123')
expect(event.data).toBeNull()
expect(event.header.controllers).toEqual(['did:key:123'])
expect(event.header.model).toBe(model)
expect(event.header.unique).toBeUndefined()
})

test('returns the deterministic event payload with the provided unique value', () => {
const model = randomStreamID()
const unique = new Uint8Array([0, 1, 2])
const event = getDeterministicInitEvent(model, 'did:key:123', unique)
expect(event.data).toBeNull()
expect(event.header.controllers).toEqual(['did:key:123'])
expect(event.header.model).toBe(model)
expect(event.header.unique).toBe(unique)
})
})

describe('createInitEvent()', () => {
test('creates unique events by adding a random unique value', async () => {
const model = randomStreamID()
const event1 = await createInitEvent({
content: { hello: 'world' },
controller: authenticatedDID,
model,
})
assertSignedEvent(event1)

const event2 = await createInitEvent({
content: { hello: 'world' },
controller: authenticatedDID,
model,
})
expect(event2).not.toEqual(event1)
})

test('adds the context and shouldIndex when if provided', async () => {
const model = randomStreamID()
const event1 = await createInitEvent({
content: { hello: 'world' },
controller: authenticatedDID,
model,
})
const payload1 = await getSignedEventPayload(
DocumentInitEventPayload,
event1,
)
expect(payload1.header.context).toBeUndefined()
expect(payload1.header.shouldIndex).toBeUndefined()

const context = randomStreamID()
const event2 = await createInitEvent({
content: { hello: 'world' },
controller: authenticatedDID,
model,
context,
shouldIndex: true,
})
const payload2 = await getSignedEventPayload(
DocumentInitEventPayload,
event2,
)
expect(payload2.header.context?.equals(context)).toBe(true)
expect(payload2.header.shouldIndex).toBe(true)
})
})

describe('createDataEvent()', () => {
const commitID = CommitID.fromStream(randomStreamID(), randomCID())

test('creates the JSON patch payload', async () => {
const event = await createDataEvent({
currentID: commitID,
currentContent: { hello: 'test' },
content: { hello: 'world', test: true },
controller: authenticatedDID,
})
const payload = await getSignedEventPayload(DocumentDataEventPayload, event)
expect(payload.data).toEqual([
{ op: 'replace', path: '/hello', value: 'world' },
{ op: 'add', path: '/test', value: true },
])
expect(payload.header).toBeUndefined()
})

test('adds the shouldIndex header when provided', async () => {
const event = await createDataEvent({
currentID: commitID,
content: { hello: 'world' },
controller: authenticatedDID,
shouldIndex: true,
})
const payload = await getSignedEventPayload(DocumentDataEventPayload, event)
expect(payload.header).toEqual({ shouldIndex: true })
})
})
69 changes: 69 additions & 0 deletions packages/document-client/test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { randomStreamID } from '@ceramic-sdk/identifiers'
import { equals } from 'uint8arrays'

import { createInitHeader } from '../src/utils.js'

describe('createInitHeader()', () => {
test('adds random unique bytes by default or when explcitly set to false', () => {
const controller = 'did:key:123'
const model = randomStreamID()

const header1 = createInitHeader({ controller, model })
expect(header1.unique).toBeInstanceOf(Uint8Array)
const header2 = createInitHeader({ controller, model })
expect(header2.unique).toBeInstanceOf(Uint8Array)
expect(
equals(header1.unique as Uint8Array, header2.unique as Uint8Array),
).toBe(false)

const header3 = createInitHeader({ controller, model, unique: false })
expect(header3.unique).toBeInstanceOf(Uint8Array)
expect(
equals(header1.unique as Uint8Array, header3.unique as Uint8Array),
).toBe(false)
})

test('adds the specified unique bytes', () => {
const controller = 'did:key:123'
const model = randomStreamID()
const unique = new Uint8Array([0, 1, 2])

const header1 = createInitHeader({ controller, model, unique })
expect(header1.unique).toBeInstanceOf(Uint8Array)
const header2 = createInitHeader({ controller, model, unique })
expect(header2.unique).toBeInstanceOf(Uint8Array)

expect(
equals(header1.unique as Uint8Array, header2.unique as Uint8Array),
).toBe(true)
})

test('does not add unique bytes if set to true', () => {
const controller = 'did:key:123'
const model = randomStreamID()
const header = createInitHeader({ controller, model, unique: true })
expect(header.unique).toBeUndefined()
})

test('does not add context and shouldIndex by default', () => {
const controller = 'did:key:123'
const model = randomStreamID()
const header = createInitHeader({ controller, model })
expect(header.context).toBeUndefined()
expect(header.shouldIndex).toBeUndefined()
})

test('adds context and shouldIndex if specified', () => {
const controller = 'did:key:123'
const model = randomStreamID()
const context = randomStreamID()
const header = createInitHeader({
controller,
model,
context,
shouldIndex: true,
})
expect(header.context?.equals(context)).toBe(true)
expect(header.shouldIndex).toBe(true)
})
})
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 998cb59

Please sign in to comment.