Skip to content

Commit

Permalink
test: add test
Browse files Browse the repository at this point in the history
  • Loading branch information
Alan Shaw committed Jun 6, 2024
1 parent 07bcf7e commit 18a2c7f
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 26 deletions.
4 changes: 2 additions & 2 deletions packages/upload-api/src/index/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const add = async ({ capability }, context) => {
// publish the index data to IPNI
context.ipniService.publish(idxRes.ok),
// publish a content claim for the index
publishIndexClaim(context, { content: idxRes.ok.content, index: idxLink })
publishIndexClaim(context, { content: idxRes.ok.content, index: idxLink }),
])
for (const res of publishRes) {
if (res.error) return res
Expand Down Expand Up @@ -99,7 +99,7 @@ const assertAllocated = async (context, space, digest, errorName) => {

/**
* @param {API.ClaimsClientContext} ctx
* @param {{ content: API.UnknownLink, index: API.CARLink }} params
* @param {{ content: API.UnknownLink, index: API.CARLink }} params
*/
const publishIndexClaim = async (ctx, { content, index }) => {
const { invocationConfig, connection } = ctx.claimsService
Expand Down
21 changes: 18 additions & 3 deletions packages/upload-api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ import { StorageGetError } from './types/storage.js'
import { AllocationsStorage, BlobsStorage, BlobAddInput } from './types/blob.js'
export type { AllocationsStorage, BlobsStorage, BlobAddInput }
import { IPNIService, IndexServiceContext } from './types/index.js'
import { ClaimsClientConfig } from './types/content-claims.js'
import { Claim } from '@web3-storage/content-claims/client/api'
export type {
IndexServiceContext,
IPNIService,
Expand All @@ -212,9 +214,10 @@ export type {
ShardedDAGIndex,
} from './types/index.js'
export type {
ClaimsInvocationConfig,
ClaimsClientContext,
Service as ClaimsService
ClaimsInvocationConfig,
ClaimsClientConfig,
ClaimsClientContext,
Service as ClaimsService,
} from './types/content-claims.js'

export interface Service extends StorefrontService, W3sService {
Expand Down Expand Up @@ -596,6 +599,18 @@ export interface UcantoServerTestContext
ipniService: IPNIService & {
query(digest: MultihashDigest): Promise<Result<Unit, RecordNotFound>>
}

carStoreBucket: CarStoreBucket & Deactivator
blobsStorage: BlobsStorage & Deactivator
claimsService: ClaimsClientConfig & ClaimReader & Deactivator
}

export interface ClaimReader {
read(digest: MultihashDigest): Promise<Result<Claim[], Failure>>
}

export interface Deactivator {
deactivate: () => Promise<void>
}

export interface StoreTestContext {}
Expand Down
18 changes: 13 additions & 5 deletions packages/upload-api/src/types/content-claims.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ConnectionView, DID, Principal, Proof, Signer } from '@ucanto/interface'
import {
ConnectionView,
DID,
Principal,
Proof,
Signer,
} from '@ucanto/interface'
import { Service } from '@web3-storage/content-claims/server/service/api'

export type { ConnectionView, DID, Principal, Proof, Signer }
Expand All @@ -15,9 +21,11 @@ export interface ClaimsInvocationConfig {
proofs?: Proof[]
}

export interface ClaimsClientConfig {
invocationConfig: ClaimsInvocationConfig
connection: ConnectionView<Service>
}

export interface ClaimsClientContext {
claimsService: {
invocationConfig: ClaimsInvocationConfig
connection: ConnectionView<Service>
}
claimsService: ClaimsClientConfig
}
131 changes: 131 additions & 0 deletions packages/upload-api/test/external-service/content-claims.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import * as API from '../../src/types.js'
import { connect } from '@ucanto/client'
import { ed25519 } from '@ucanto/principal'
import { CAR, HTTP } from '@ucanto/transport'
import { Assert } from '@web3-storage/content-claims/capability'
import * as Client from '@web3-storage/content-claims/client'
import * as Server from '@web3-storage/content-claims/server'
import { DigestMap } from '@web3-storage/blob-index'

/**
* @param {object} params
* @param {API.Signer} params.serviceSigner
* @param {API.Transport.Channel<API.ClaimsService>} params.channel
* @returns {Promise<API.ClaimsClientConfig>}
*/
export const create = async ({ serviceSigner, channel }) => {
const agent = await ed25519.generate()
const proofs = [
await Assert.assert.delegate({
issuer: serviceSigner,
with: serviceSigner.did(),
audience: agent,
}),
]
return {
invocationConfig: {
issuer: agent,
with: serviceSigner.did(),
audience: serviceSigner,
proofs,
},
connection: connect({
id: serviceSigner,
codec: CAR.outbound,
channel,
}),
}
}

/**
* @param {{ http?: import('node:http') }} [options]
* @returns {Promise<API.ClaimsClientConfig & API.ClaimReader & API.Deactivator>}
*/
export const activate = async ({ http } = {}) => {
const serviceSigner = await ed25519.generate()

const claimStore = new ClaimStorage()
/** @param {API.MultihashDigest} content */
const read = async (content) => {
/** @type {import('@web3-storage/content-claims/client/api').Claim[]} */
const claims = []
await Server.walkClaims(
{ claimFetcher: claimStore },
content,
new Set()
).pipeTo(
new WritableStream({
async write(block) {
const claim = await Client.decode(block.bytes)
claims.push(claim)
},
})
)
return { ok: claims }
}

const server = Server.createServer({
id: serviceSigner,
codec: CAR.inbound,
claimStore,
validateAuthorization: () => ({ ok: {} }),
})

if (!http) {
const conf = await create({ serviceSigner, channel: server })
return Object.assign(conf, { read, deactivate: async () => {} })
}

const httpServer = http.createServer(async (req, res) => {
const chunks = []
for await (const chunk of req) {
chunks.push(chunk)
}

const { headers, body } = await server.request({
// @ts-expect-error
headers: req.headers,
body: new Uint8Array(await new Blob(chunks).arrayBuffer()),
})

res.writeHead(200, headers)
res.write(body)
res.end()
})
await new Promise((resolve) => httpServer.listen(resolve))
// @ts-expect-error
const { port } = httpServer.address()
const serviceURL = new URL(`http://127.0.0.1:${port}`)

const channel = HTTP.open({ url: serviceURL, method: 'POST' })
const conf = await create({ serviceSigner, channel })
return Object.assign(conf, {
read,
deactivate: () =>
new Promise((resolve, reject) => {
httpServer.closeAllConnections()
httpServer.close((err) => {
err ? reject(err) : resolve(undefined)
})
}),
})
}

class ClaimStorage {
constructor() {
/** @type {Map<API.MultihashDigest, import('@web3-storage/content-claims/server/api').Claim[]>} */
this.data = new DigestMap()
}

/** @param {import('@web3-storage/content-claims/server/api').Claim} claim */
async put(claim) {
const claims = this.data.get(claim.content) ?? []
claims.push(claim)
this.data.set(claim.content, claims)
}

/** @param {API.MultihashDigest} content */
async get(content) {
return this.data.get(content) ?? []
}
}
10 changes: 8 additions & 2 deletions packages/upload-api/test/external-service/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { IPNIService } from './ipni-service.js'
import { IPNIService } from './ipni.js'
import * as ClaimsService from './content-claims.js'

export const getExternalServiceImplementations = async () => ({
/**
* @param {object} [options]
* @param {import('node:http')} [options.http]
*/
export const getExternalServiceImplementations = async (options) => ({
ipniService: new IPNIService(),
claimsService: await ClaimsService.activate(options),
})
74 changes: 74 additions & 0 deletions packages/upload-api/test/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,78 @@ export const test = {
assert.ok(receipt.out.error)
assert.equal(receipt.out.error?.name, 'ShardNotFound')
},
'index/add should publish index claim': async (assert, context) => {
const { proof, spaceDid } = await registerSpace(alice, context)
const contentCAR = await randomCAR(32)
const contentCARBytes = new Uint8Array(await contentCAR.arrayBuffer())

const connection = connect({
id: context.id,
channel: createServer(context),
})

// upload the content CAR to the space
await uploadBlob(
context,
{
connection,
issuer: alice,
audience: context.id,
with: spaceDid,
proofs: [proof],
},
{
cid: contentCAR.cid,
bytes: contentCARBytes,
}
)

const index = await fromShardArchives(contentCAR.roots[0], [
contentCARBytes,
])
const indexCAR = Result.unwrap(await index.archive())
const indexLink = await CAR.link(indexCAR)

// upload the index CAR to the space
await uploadBlob(
context,
{
connection,
issuer: alice,
audience: context.id,
with: spaceDid,
proofs: [proof],
},
{
cid: indexLink,
bytes: indexCAR,
}
)

const indexAdd = IndexCapabilities.add.invoke({
issuer: alice,
audience: context.id,
with: spaceDid,
nb: { index: indexLink },
proofs: [proof],
})
const receipt = await indexAdd.execute(connection)
Result.try(receipt.out)

// ensure an index claim exists for the content root
const claims = Result.unwrap(
await context.claimsService.read(contentCAR.roots[0].multihash)
)

let found = false
for (const c of claims) {
if (
c.type === 'assert/index' &&
c.index.toString() === indexLink.toString()
) {
found = true
}
}
assert.ok(found, 'did not found index claim')
},
}
22 changes: 8 additions & 14 deletions packages/upload-api/test/helpers/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import {
getStoreImplementations as getFilecoinStoreImplementations,
getQueueImplementations as getFilecoinQueueImplementations,
} from '@web3-storage/filecoin-api/test/context/service'
import { BlobsStorage } from '../storage/blobs-storage.js'
import { CarStoreBucket } from '../storage/car-store-bucket.js'
import * as Email from '../../src/utils/email.js'
import { create as createRevocationChecker } from '../../src/utils/revocation.js'
import { createServer, connect } from '../../src/lib.js'
Expand Down Expand Up @@ -51,7 +49,7 @@ export const createContext = async (
} = getFilecoinStoreImplementations()
const email = Email.debug()

const externalServices = await getExternalServiceImplementations()
const externalServices = await getExternalServiceImplementations(options)

/** @type { import('../../src/types.js').UcantoServerContext } */
const serviceContext = {
Expand Down Expand Up @@ -100,6 +98,7 @@ export const createContext = async (

return {
...serviceContext,
...serviceStores,
...externalServices,
mail: /** @type {TestTypes.DebugEmail} */ (serviceContext.email),
service: /** @type {TestTypes.ServiceSigner} */ (serviceContext.id),
Expand All @@ -113,14 +112,9 @@ export const createContext = async (
*
* @param {Types.UcantoServerTestContext} context
*/
export const cleanupContext = async (context) => {
/** @type {CarStoreBucket & { deactivate: () => Promise<void> }}} */
// @ts-ignore type misses S3 bucket properties like accessKey
const carStoreBucket = context.carStoreBucket
await carStoreBucket.deactivate()

/** @type {BlobsStorage & { deactivate: () => Promise<void> }}} */
// @ts-ignore type misses S3 bucket properties like accessKey
const blobsStorage = context.blobsStorage
await blobsStorage.deactivate()
}
export const cleanupContext = (context) =>
Promise.all([
context.carStoreBucket.deactivate(),
context.blobsStorage.deactivate(),
context.claimsService.deactivate(),
])

0 comments on commit 18a2c7f

Please sign in to comment.