diff --git a/.github/workflows/hermetic.yml b/.github/workflows/hermetic.yml index ac56483c..a9edaff5 100644 --- a/.github/workflows/hermetic.yml +++ b/.github/workflows/hermetic.yml @@ -135,12 +135,46 @@ jobs: test_selector="fast" fi make TEST_SELECTOR="$test_selector" HERMETIC_CMD=./bin/hermetic-driver hermetic-tests + run-migration-tests: + name: Test Migrations + runs-on: ubuntu-latest + environment: test + needs: + - build-driver + - publish-suite + - generate-matrix #Needed to know the BUILD_TAG + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Setup GKE auth + uses: 'google-github-actions/auth@v1' + with: + credentials_json: ${{ secrets.GKE_SA_KEY }} + - + name: Get GKE credentials + uses: 'google-github-actions/get-gke-credentials@v1' + with: + cluster_name: ${{ vars.GKE_CLUSTER }} + location: ${{ vars.GKE_ZONE }} + - uses: actions/download-artifact@master + with: + name: hermetic-driver + path: ./bin + - + name: Test ${{ matrix.networks }} + run: | + set -euxo pipefail + export BUILD_TAG=${{ needs.generate-matrix.outputs.build_tag }} + chmod +x ./bin/hermetic-driver + make HERMETIC_CMD=./bin/hermetic-driver migration-tests collect-results: name: Hermetic Test Results if: ${{ always() }} runs-on: ubuntu-latest - needs: [run-tests] + needs: [run-tests, run-migration-tests] steps: - run: exit 1 # see https://stackoverflow.com/a/67532120/4907315 diff --git a/Cargo.lock b/Cargo.lock index 83c344ba..185c833f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,7 +853,7 @@ dependencies = [ [[package]] name = "keramik-common" version = "0.1.0" -source = "git+https://github.com/3box/keramik.git?branch=main#4e466b491f470042cc8b6a0130898b3bb8698139" +source = "git+https://github.com/3box/keramik.git?branch=main#4612921db9e7e2941a04ca114c6ea56f01ce5b1b" dependencies = [ "anyhow", "gethostname", @@ -864,7 +864,7 @@ dependencies = [ [[package]] name = "keramik-operator" version = "0.0.1" -source = "git+https://github.com/3box/keramik.git?branch=main#4e466b491f470042cc8b6a0130898b3bb8698139" +source = "git+https://github.com/3box/keramik.git?branch=main#4612921db9e7e2941a04ca114c6ea56f01ce5b1b" dependencies = [ "k8s-openapi", "keramik-common", diff --git a/Makefile b/Makefile index 42c64a25..d612051b 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ BUILD_PROFILE ?= release BUILD_TAG ?= dev-run # Path to network to test against. TEST_NETWORK ?= ./networks/basic-rust.yaml +# Path to migration network to test against. +TEST_PRE_MIGRATION_NETWORK ?= ./migration-networks/basic-go-rust-pre.yaml +# Path to migration network to test against. +TEST_POST_MIGRATION_NETWORK ?= ./migration-networks/basic-go-rust-post.yaml # Path to performance simulation. TEST_SIMULATION ?= ./simulations/basic-simulation.yaml # Name for the test suite image, without any tag @@ -104,6 +108,18 @@ hermetic-tests: --test-image "${TEST_SUITE_IMAGE}" \ --test-selector "${TEST_SELECTOR}" +.PHONY: migration-tests +migration-tests: + ${HERMETIC_CMD} test \ + --network "${TEST_PRE_MIGRATION_NETWORK}" \ + --flavor migration \ + --suffix "${HERMETIC_SUFFIX}" \ + --network-ttl ${HERMETIC_TTL} \ + --test-image "${TEST_SUITE_IMAGE}" \ + --test-selector "migration" \ + --migration-wait-secs 400 \ + --migration-network "${TEST_POST_MIGRATION_NETWORK}" + .PHONY: performance-tests performance-tests: ${HERMETIC_CMD} test \ diff --git a/README.md b/README.md index 83fb38d8..9bff3dbd 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,18 @@ Testing against durable infrastructure allows for better coverage of real world There are several test flavors: * Correctness tests +* Migration tests * Performance tests More will be added as needed. The correctness tests, test a specific property of a network (i.e. writes can be read). These tests do not assume any network topology. -Property tests live in this repo in the `/suite` directory. +Correctness tests live in this repo in the `/suite` directory. + +The migration tests run tests specific to migrating from older versions to newer versions of Ceramic. +These tests assume that when the version of the Ceramic process changes the migration is complete and the test can continue to validate the migration. +Migrations tests live in this repo in the `/suite/src/__tests__/migration` directory. The performance tests, test at scale. These tests do not assume any network topology. diff --git a/hermetic/src/cli.rs b/hermetic/src/cli.rs index 2620a575..a2512e7f 100644 --- a/hermetic/src/cli.rs +++ b/hermetic/src/cli.rs @@ -60,12 +60,25 @@ pub struct TestOpts { /// Path regex passed to Jest to select which tests to run. #[arg(long, default_value = ".")] test_selector: String, + + /// Path to migration network yaml file. + /// Required with flavor is `migration`. + #[arg(long)] + migration_network: Option, + + /// Number of seconds to wait after starting the test job before starting the migration + /// network. + /// Required with flavor is `migration`. + #[arg(long)] + migration_wait_secs: Option, } #[derive(Debug, Clone)] pub enum FlavorOpts { /// Correctness tests Correctness, + /// Migration tests + Migration, /// Performance tests Performance, } @@ -74,6 +87,7 @@ impl FlavorOpts { fn name(&self) -> &'static str { match self { FlavorOpts::Correctness => "correctness", + FlavorOpts::Migration => "migration", FlavorOpts::Performance => "perf", } } @@ -81,7 +95,11 @@ impl FlavorOpts { impl ValueEnum for FlavorOpts { fn value_variants<'a>() -> &'a [Self] { - &[FlavorOpts::Correctness, FlavorOpts::Performance] + &[ + FlavorOpts::Correctness, + FlavorOpts::Migration, + FlavorOpts::Performance, + ] } fn to_possible_value(&self) -> Option { @@ -109,10 +127,18 @@ impl TryFrom for TestConfig { network_timeout, job_timeout, test_selector, + migration_network, + migration_wait_secs, } = opts; let flavor = match flavor { FlavorOpts::Correctness => Flavor::Correctness, + FlavorOpts::Migration => Flavor::Migration { + wait_secs: migration_wait_secs + .ok_or(anyhow!("Migration flavor requires `migration_wait_secs`"))?, + migration: migration_network + .ok_or(anyhow!("Migration flavor requires `migration_network`"))?, + }, FlavorOpts::Performance => Flavor::Performance( simulation.ok_or(anyhow!("Simulation file required for performance tests"))?, ), diff --git a/hermetic/src/cli/tester.rs b/hermetic/src/cli/tester.rs index bc8033f6..013ab18a 100644 --- a/hermetic/src/cli/tester.rs +++ b/hermetic/src/cli/tester.rs @@ -1,4 +1,9 @@ -use std::{collections::BTreeMap, fmt::Display, path::PathBuf}; +use std::{ + collections::BTreeMap, + fmt::Display, + path::{Path, PathBuf}, + time::Duration, +}; use anyhow::{anyhow, Result}; use futures::{future::BoxFuture, StreamExt, TryStreamExt}; @@ -32,7 +37,7 @@ use kube::{ }; use log::{debug, info, trace}; use serde::{de::DeserializeOwned, Serialize}; -use tokio::fs; +use tokio::{fs, time::sleep}; const TESTER_NAME: &str = "ceramic-tester"; const CERAMIC_ADMIN_DID_SECRET_NAME: &str = "ceramic-admin"; @@ -73,6 +78,8 @@ pub struct TestConfig { pub enum Flavor { /// Correctness tests Correctness, + /// Migration tests + Migration { wait_secs: u64, migration: PathBuf }, /// Performance tests Performance(PathBuf), } @@ -81,6 +88,7 @@ impl Flavor { fn name(&self) -> &'static str { match self { Flavor::Correctness => "correctness", + Flavor::Migration { .. } => "migration", Flavor::Performance(_) => "perf", } } @@ -92,34 +100,47 @@ impl Display for Flavor { } } -pub async fn run(opts: TestConfig) -> Result<()> { - // Infer the runtime environment and try to create a Kubernetes Client - let client = Client::try_default().await?; - +async fn parse_network_file( + file_path: impl AsRef, + ttl: u64, + flavor: &Flavor, + suffix: &Option, +) -> Result { // Parse network file - let mut network: Network = serde_yaml::from_str(&fs::read_to_string(&opts.network).await?)?; + let mut network: Network = serde_yaml::from_str(&fs::read_to_string(file_path).await?)?; debug!("input network {:#?}", network); // The test driver relies on the Keramik operator network TTL to clean up the network, with an 8 hour default that // allows devs to investigate any failures. The TTL can also be extended at any time, if more time is needed. - network.spec.ttl_seconds = Some(opts.network_ttl); + network.spec.ttl_seconds = Some(ttl); let mut network_name = format!( "{}-{}", - opts.flavor, + flavor, network .metadata .name .as_ref() .expect("network should have a defined name in metadata") ); - if let Some(suffix) = &opts.suffix { + if let Some(suffix) = &suffix { if !suffix.is_empty() { network_name = format!("{network_name}-{suffix}"); } } network.metadata.name = Some(network_name.clone()); + Ok(network) +} + +pub async fn run(opts: TestConfig) -> Result<()> { + // Infer the runtime environment and try to create a Kubernetes Client + let client = Client::try_default().await?; + + // Parse network file + let network = + parse_network_file(opts.network, opts.network_ttl, &opts.flavor, &opts.suffix).await?; debug!("configured network {:#?}", network); + let network_name = network.name_unchecked(); let namespace = format!("keramik-{network_name}"); @@ -166,7 +187,7 @@ pub async fn run(opts: TestConfig) -> Result<()> { // Create any dependencies of the job match opts.flavor { - Flavor::Performance(_) => {} + Flavor::Performance(_) | Flavor::Migration { .. } => {} Flavor::Correctness => { apply_resource_namespaced( client.clone(), @@ -184,7 +205,7 @@ pub async fn run(opts: TestConfig) -> Result<()> { // Create the job/simulation let job_name = match &opts.flavor { - Flavor::Correctness => { + Flavor::Correctness | Flavor::Migration { .. } => { create_resource_namespaced( client.clone(), &namespace, @@ -206,6 +227,21 @@ pub async fn run(opts: TestConfig) -> Result<()> { Flavor::Correctness => { wait_for_job(client.clone(), &namespace, &job_name, opts.job_timeout).await? } + Flavor::Migration { + wait_secs, + ref migration, + } => { + // Wait designated time before starting the migration + sleep(Duration::from_secs(wait_secs)).await; + let migration_network = + parse_network_file(migration, opts.network_ttl, &opts.flavor, &opts.suffix).await?; + + // Apply the migration network + apply_resource(client.clone(), migration_network).await?; + + // Finally wait for the test job to finish + wait_for_job(client.clone(), &namespace, &job_name, opts.job_timeout).await? + } Flavor::Performance(_) => { wait_for_simulation(client.clone(), &namespace, opts.job_timeout).await? } diff --git a/migration-networks/basic-go-rust-post.yaml b/migration-networks/basic-go-rust-post.yaml new file mode 100644 index 00000000..94b9ea4e --- /dev/null +++ b/migration-networks/basic-go-rust-post.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: "keramik.3box.io/v1alpha1" +kind: Network +metadata: + name: basic-go-rust +spec: + replicas: 1 + ceramic: + - env: + CERAMIC_RECON_MODE: "true" + ipfs: + rust: + env: + CERAMIC_ONE_RECON: "true" + resourceLimits: + cpu: "4" + memory: "1Gi" + migrationCmd: + - "from-ipfs" + - "-i" + - "/data/ipfs/blocks" + - "-o" + - "/data/ipfs/" + - "--network" + - "dev-unstable" + # Use Kubo with CAS because it still needs pubsub + cas: + casResourceLimits: + cpu: "2" + memory: "4Gi" + ipfs: + go: {} diff --git a/migration-networks/basic-go-rust-pre.yaml b/migration-networks/basic-go-rust-pre.yaml new file mode 100644 index 00000000..7e0e09f3 --- /dev/null +++ b/migration-networks/basic-go-rust-pre.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: "keramik.3box.io/v1alpha1" +kind: Network +metadata: + name: basic-go-rust +spec: + replicas: 1 + ceramic: + - ipfs: + go: + resourceLimits: + cpu: "4" + memory: "1Gi" + # Use Kubo with CAS because it still needs pubsub + cas: + casResourceLimits: + cpu: "2" + memory: "4Gi" + ipfs: + go: {} diff --git a/suite/package.json b/suite/package.json index e5112f1a..e1035bd7 100644 --- a/suite/package.json +++ b/suite/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "tsc", - "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand --reporters default --setupFiles dotenv/config", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand --reporters default --setupFiles dotenv/config --forceExit", "format": "prettier --write '**/*.{js,ts,jsx,tsx,json,css,scss,md}'" }, "devDependencies": { diff --git a/suite/src/__tests__/fast/all-event-types.tests.ts b/suite/src/__tests__/fast/all-event-types.tests.ts deleted file mode 100644 index 39da08ab..00000000 --- a/suite/src/__tests__/fast/all-event-types.tests.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { CeramicClient } from '@ceramicnetwork/http-client' -import { beforeAll, test, describe, expect } from '@jest/globals' -import { Model } from '@ceramicnetwork/stream-model' -import { AccountId } from 'caip' -import { HDNodeWallet, Wallet } from 'ethers' -import { EthereumNodeAuth } from '@didtools/pkh-ethereum' -import { DIDSession } from 'did-session' -import { StreamID } from '@ceramicnetwork/streamid' -import { ModelInstanceDocument } from '@ceramicnetwork/stream-model-instance' -import * as u8s from 'uint8arrays' - -import { newCeramic } from '../../utils/ceramicHelpers.js' -import { createDid } from '../../utils/didHelper.js' -import { SINGLE_MODEL_DEFINITION, LIST_MODEL_DEFINITION } from '../../models/modelConstants.js' -import { indexModelOnNode } from '../../utils/composeDbHelpers.js' - -// Environment variables -const composeDbUrls = String(process.env.COMPOSEDB_URLS).split(',') -const adminSeeds = String(process.env.COMPOSEDB_ADMIN_DID_SEEDS).split(',') - -class MockProvider { - wallet: HDNodeWallet - - constructor(wallet: HDNodeWallet) { - this.wallet = wallet - } - - send( - request: { method: string; params: Array }, - callback: (err: Error | null | undefined, res?: any) => void, - ): void { - if (request.method === 'eth_chainId') { - callback(null, { result: '1' }) - } else if (request.method === 'personal_sign') { - let message = request.params[0] as string - if (message.startsWith('0x')) { - message = u8s.toString(u8s.fromString(message.slice(2), 'base16'), 'utf8') - } - callback(null, { result: this.wallet.signMessage(message) }) - } else { - callback(new Error(`Unsupported method: ${request.method}`)) - } - } -} - -describe('All Event Types', () => { - let ceramic: CeramicClient - let singleModelId: StreamID - let listModelId: StreamID - - beforeAll(async () => { - const did = await createDid(adminSeeds[0]) - ceramic = await newCeramic(composeDbUrls[0], did) - - const singleModel = await Model.create(ceramic, SINGLE_MODEL_DEFINITION) - singleModelId = singleModel.id - await indexModelOnNode(ceramic, singleModelId) - - const listModel = await Model.create(ceramic, LIST_MODEL_DEFINITION) - listModelId = listModel.id - await indexModelOnNode(ceramic, listModelId) - }) - - //time events are covered through the anchor test - - test('did:key signed', async () => { - // did:key - const did = await createDid() - ceramic.did = did - - // did:key signed init event - const content = { step: 400 } - const metadata = { controllers: [did.id], model: listModelId } - const doc = await ModelInstanceDocument.create(ceramic, content, metadata, { anchor: false }) - - const loadedDoc = await ModelInstanceDocument.load(ceramic, doc.id) - expect(loadedDoc.content).toEqual(content) - - // did:key signed data event - const content2 = { step: 401 } - await doc.replace(content2, null, { anchor: false }) - - // can read - await loadedDoc.sync() - expect(loadedDoc.content).toEqual(content2) - }) - - test('cacao signed', async () => { - // did:pkh + cacao - const wallet = Wallet.createRandom() - const provider = new MockProvider(wallet) - const accountId = new AccountId({ - address: wallet.address.toLowerCase(), - chainId: { namespace: 'eip155', reference: '1' }, - }) - const authMethod = await EthereumNodeAuth.getAuthMethod(provider, accountId, 'test') - const resources = [`ceramic://*`] - const session = await DIDSession.authorize(authMethod, { - resources, - }) - ceramic.did = session.did - - // cacao signed init event - const content = { step: 600 } - const metadata = { - controllers: [`did:pkh:eip155:1:${wallet.address.toLowerCase()}`], - model: listModelId, - } - const doc = await ModelInstanceDocument.create(ceramic, content, metadata, { anchor: false }) - - const loadedDoc = await ModelInstanceDocument.load(ceramic, doc.id) - expect(loadedDoc.content).toEqual(content) - - // cacao signed data event - const content2 = { step: 601 } - await doc.replace(content2, null, { anchor: false }) - - await loadedDoc.sync() - expect(loadedDoc.content).toEqual(content2) - }) - - test('unsigned ', async () => { - const did = await createDid() - ceramic.did = did - - // single/deterministic model instance documents are unsigned - const metadata = { controllers: [did.id], model: singleModelId, deterministic: true } - const doc = await ModelInstanceDocument.single(ceramic, metadata, { - anchor: false, - }) - - // did:key signed data event - const content = { step: 700 } - await doc.replace(content) - - const loadedDoc = await ModelInstanceDocument.load(ceramic, doc.id) - expect(loadedDoc.content).toEqual(content) - }) -}) diff --git a/suite/src/__tests__/migration/all-event-types.tests.ts b/suite/src/__tests__/migration/all-event-types.tests.ts new file mode 100644 index 00000000..b52fd514 --- /dev/null +++ b/suite/src/__tests__/migration/all-event-types.tests.ts @@ -0,0 +1,233 @@ +import { CeramicClient } from '@ceramicnetwork/http-client' +import { utilities } from '../../utils/common.js' +import { test, describe, expect } from '@jest/globals' +import { Model } from '@ceramicnetwork/stream-model' +import { AccountId } from 'caip' +import { HDNodeWallet, Wallet } from 'ethers' +import { EthereumNodeAuth } from '@didtools/pkh-ethereum' +import { DIDSession } from 'did-session' +import { StreamID } from '@ceramicnetwork/streamid' +import { ModelInstanceDocument } from '@ceramicnetwork/stream-model-instance' +import * as u8s from 'uint8arrays' + +import { newCeramic, waitForAnchor } from '../../utils/ceramicHelpers.js' +import { createDid } from '../../utils/didHelper.js' +import { SINGLE_MODEL_DEFINITION, LIST_MODEL_DEFINITION } from '../../models/modelConstants.js' +import { indexModelOnNode } from '../../utils/composeDbHelpers.js' + +const delay = utilities.delay + + +// Environment variables +const ceramicUrls = String(process.env.CERAMIC_URLS).split(',') +const composeDbUrls = String(process.env.COMPOSEDB_URLS).split(',') +const adminSeeds = String(process.env.COMPOSEDB_ADMIN_DID_SEEDS).split(',') + +class MockProvider { + wallet: HDNodeWallet + + constructor(wallet: HDNodeWallet) { + this.wallet = wallet + } + + send( + request: { method: string; params: Array }, + callback: (err: Error | null | undefined, res?: any) => void, + ): void { + if (request.method === 'eth_chainId') { + callback(null, { result: '1' }) + } else if (request.method === 'personal_sign') { + let message = request.params[0] as string + if (message.startsWith('0x')) { + message = u8s.toString(u8s.fromString(message.slice(2), 'base16'), 'utf8') + } + callback(null, { result: this.wallet.signMessage(message) }) + } else { + callback(new Error(`Unsupported method: ${request.method}`)) + } + } +} + +async function getVersion(url: string) { + try { + let response = await fetch(url + `/api/v0/id`, { method: 'POST' }) + let info = await response.json() + return info.AgentVersion + } catch { + return undefined + } +} +async function waitForVersionChange(url: string, prevVersion: string) { + let version = await getVersion(url) + while (version === undefined || version === prevVersion) { + console.log('waiting for version change', prevVersion) + await delay(5) + version = await getVersion(url) + } + console.log('version changed', version) +} + +async function waitForNodeAlive(url: string) { + while (true) { + try { + console.log('waiting for node to be alive') + let response = await fetch(url + `/api/v0/node/healthcheck`) + let info = await response.text() + if (info == 'Alive!') { + return + } + await delay(5) + } catch { + //Ignore all other errors + } + } +} + +async function writeKeySignedDataEvents(url: string, listModelId: StreamID) { + // did:key + const did = await createDid() + let ceramic = await newCeramic(url, did) + + // did:key signed init event + const content = { step: 400 } + const metadata = { controllers: [did.id], model: listModelId } + const doc = await ModelInstanceDocument.create(ceramic, content, metadata, { anchor: true }) + + // Ensure the init event is anchored + await waitForAnchor(doc) + + const loadedDoc = await ModelInstanceDocument.load(ceramic, doc.id) + expect(loadedDoc.content).toEqual(content) + + // did:key signed data event + const content2 = { step: 401 } + await doc.replace(content2, null, { anchor: true }) + + // Ensure the data event is anchored + await waitForAnchor(doc) + + return { doc: loadedDoc as ModelInstanceDocument>, content: content2 } +} + +async function readKeySignedDataEvents(loadedDoc: ModelInstanceDocument>, content: Record) { + await loadedDoc.sync() + expect(loadedDoc.content).toEqual(content) +} + + + +async function writeCACAOSignedDataEvents(url: string, listModelId: StreamID) { + // did:pkh + cacao + const wallet = Wallet.createRandom() + const provider = new MockProvider(wallet) + const accountId = new AccountId({ + address: wallet.address.toLowerCase(), + chainId: { namespace: 'eip155', reference: '1' }, + }) + const authMethod = await EthereumNodeAuth.getAuthMethod(provider, accountId, 'test') + const resources = [`ceramic://*`] + const session = await DIDSession.authorize(authMethod, { + resources, + }) + let ceramic = await newCeramic(url, session.did) + + // cacao signed init event + const content = { step: 600 } + const metadata = { + controllers: [`did:pkh:eip155:1:${wallet.address.toLowerCase()}`], + model: listModelId, + } + const doc = await ModelInstanceDocument.create(ceramic, content, metadata, { anchor: true }) + + // Ensure the init event is anchored + await waitForAnchor(doc) + + const loadedDoc = await ModelInstanceDocument.load(ceramic, doc.id) + expect(loadedDoc.content).toEqual(content) + + // cacao signed data event + const content2 = { step: 601 } + await doc.replace(content2, null, { anchor: true }) + + // Ensure the data event is anchored + await waitForAnchor(doc) + + return { doc: loadedDoc as ModelInstanceDocument>, content: content2 } +} +async function readCACAOSignedDataEvents(loadedDoc: ModelInstanceDocument>, content: Record) { + await loadedDoc.sync() + expect(loadedDoc.content).toEqual(content) +} + + +async function writeUnsignedInitEvents(url: string, singleModelId: StreamID) { + const did = await createDid() + let ceramic = await newCeramic(url, did) + + // single/deterministic model instance documents are unsigned + const metadata = { controllers: [did.id], model: singleModelId, deterministic: true } + const doc = await ModelInstanceDocument.single(ceramic, metadata, { + anchor: true, + }) + + // Ensure the init event is anchored + await waitForAnchor(doc) + + // did:key signed data event + const content = { step: 700 } + await doc.replace(content, null, { anchor: true }) + + // Ensure the data event is anchored + await waitForAnchor(doc) + + return { + ceramic, + id: doc.id, + content + } +} + +async function readUnsignedInitEvents(ceramic: CeramicClient, id: StreamID, content: Record) { + const loadedDoc = await ModelInstanceDocument.load(ceramic, id) + expect(loadedDoc.content).toEqual(content) +} + +describe('All Event Types', () => { + let ceramic: CeramicClient + let singleModelId: StreamID + let listModelId: StreamID + let ceramicVersion: string + + test('migrate', async () => { + // Setup client and models + ceramicVersion = await getVersion(ceramicUrls[0]) + const did = await createDid(adminSeeds[0]) + ceramic = await newCeramic(composeDbUrls[0], did) + + const singleModel = await Model.create(ceramic, SINGLE_MODEL_DEFINITION) + singleModelId = singleModel.id + await indexModelOnNode(ceramic, singleModelId) + + const listModel = await Model.create(ceramic, LIST_MODEL_DEFINITION) + listModelId = listModel.id + await indexModelOnNode(ceramic, listModelId) + + // Write data of all event types + // Each write is anchored ensuring we have time events with a prev pointing to all types of events + let [key, cacao, unsigned] = await Promise.all([ + writeKeySignedDataEvents(composeDbUrls[0], listModelId), + writeCACAOSignedDataEvents(composeDbUrls[0], listModelId), + writeUnsignedInitEvents(composeDbUrls[0], singleModelId) + ]); + + // Wait for the version change to indicate a migration has completed + await waitForVersionChange(ceramicUrls[0], ceramicVersion) + // Wait for js-ceramic node to be alive before continuing + await waitForNodeAlive(composeDbUrls[0]) + + // Read previously written data + await readKeySignedDataEvents(key.doc, key.content) + await readCACAOSignedDataEvents(cacao.doc, cacao.content) + await readUnsignedInitEvents(unsigned.ceramic, unsigned.id, unsigned.content) + }) +}) diff --git a/suite/src/utils/ceramicHelpers.ts b/suite/src/utils/ceramicHelpers.ts index 54f3c824..03d43f5b 100644 --- a/suite/src/utils/ceramicHelpers.ts +++ b/suite/src/utils/ceramicHelpers.ts @@ -116,8 +116,9 @@ export async function waitForCondition( await withTimeout(waiter, timeoutMs) } + const curTime = new Date().toISOString() console.debug( - `Stream ${stream.id.toString()} successfully reached desired state. Current stream state: ${JSON.stringify( + `Stream ${stream.id.toString()} successfully reached desired state at ${curTime}. Current stream state: ${JSON.stringify( StreamUtils.serializeState(stream.state), )}`, ) @@ -127,7 +128,7 @@ export async function waitForAnchor( stream: any, timeoutMs: number = DEFAULT_ANCHOR_TIMEOUT_MS, ): Promise { - const msgGenerator = function (stream: Stream) { + const msgGenerator = function(stream: Stream) { const curTime = new Date().toISOString() return `Waiting for stream ${stream.id.toString()} to be anchored. Current time: ${curTime}. Current stream state: ${JSON.stringify( StreamUtils.serializeState(stream.state), @@ -135,7 +136,7 @@ export async function waitForAnchor( } await waitForCondition( stream, - function (state) { + function(state) { return state.anchorStatus == AnchorStatus.ANCHORED }, timeoutMs,