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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"scripts": {
"dev": "pnpm run -r dev",
"build": "pnpm run -r build",
"prepare": "pnpm build",
"prepare": "pnpm --filter !@tsky/docs build",
"docs:dev": "pnpm run --filter @tsky/docs dev",
"docs:build": "pnpm run --filter @tsky/docs build",
"docs:preview": "pnpm run --filter @tsky/docs preview",
Expand Down
46 changes: 46 additions & 0 deletions packages/client/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { TestProject } from 'vitest/node';

import { CredentialManager, XRPC } from '@atcute/client';
import { TestNetwork } from '@atcute/internal-dev-env';

let network: TestNetwork;

export async function setup(project: TestProject) {
network = await TestNetwork.create({});
console.log(
`🌐 Created test network:\n- pds: ${network.pds.url}\n- plc: ${network.plc.url}`,
);

const manager = new CredentialManager({ service: network.pds.url });
const rpc = new XRPC({
handler: manager,
});

await createAccount(rpc, 'alice.test');
await createAccount(rpc, 'bob.test');

project.provide('testPdsUrl', network.pds.url);
project.provide('testPlcUrl', network.plc.url);
}

export async function teardown() {
await network.close();
}

const createAccount = async (rpc: XRPC, handle: string) => {
await rpc.call('com.atproto.server.createAccount', {
data: {
handle: handle,
email: `${handle}@example.com`,
password: 'password',
},
});
console.log(`🙋 Created new account: @${handle}`);
};

declare module 'vitest' {
export interface ProvidedContext {
testPdsUrl: string;
testPlcUrl: string;
}
}
5 changes: 3 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@
"@atcute/client": "^2.0.6"
},
"devDependencies": {
"@atcute/internal-dev-env": "workspace:*",
"@tsky/lexicons": "workspace:*",
"@vitest/coverage-istanbul": "2.1.6",
"globals": "^15.12.0",
"pkgroll": "^2.5.1",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"vitest": "^2.1.6"
"typescript": "catalog:",
"vitest": "^3.0.5"
}
}
45 changes: 45 additions & 0 deletions packages/client/src/tsky/tsky.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, inject, it } from 'vitest';

import type { AtpSessionData } from '@atcute/client';

import { createAgent } from '~/tsky';

describe('createAgent', () => {
it('can create agent for Alice', async () => {
const agent = await createAgent(
{
identifier: 'alice.test',
password: 'password',
},
{ service: inject('testPdsUrl') },
);
expect(agent.session).not.toBe(undefined);
expect(agent.session?.handle).toBe('alice.test');
expect(agent.session?.email).toBe('alice.test@example.com');
});

it('can resume from stored session', async () => {
let session: AtpSessionData;
{
const agent = await createAgent(
{
identifier: 'alice.test',
password: 'password',
},
{ service: inject('testPdsUrl') },
);
expect(agent.session).toBeDefined();
session = agent.session as AtpSessionData;
}

{
const agent = await createAgent(
{ session },
{ service: inject('testPdsUrl') },
);
expect(agent.session).not.toBe(undefined);
expect(agent.session?.handle).toBe('alice.test');
expect(agent.session?.email).toBe('alice.test@example.com');
}
});
});
7 changes: 5 additions & 2 deletions packages/client/src/tsky/tsky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ export async function createAgent(
options ?? { service: 'https://bsky.social' },
);

if ('session' in credentials) manager.resume(credentials.session);
else await manager.login(credentials);
if ('session' in credentials) {
await manager.resume(credentials.session);
} else {
await manager.login(credentials);
}

return new Agent(manager);
}
30 changes: 30 additions & 0 deletions packages/client/test-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { inject } from 'vitest';

import type { Agent } from '~/agent';
import { createAgent } from '~/tsky';

type Handle = 'alice.test' | 'bob.test';

const testAgents: Record<Handle, Agent> = {
'alice.test': await createTestAgent('alice.test'),
'bob.test': await createTestAgent('bob.test'),
};

function createTestAgent(handle: Handle) {
return createAgent(
{
identifier: handle,
password: 'password',
},
{ service: inject('testPdsUrl') },
);
}

/**
* Get Agent instance for testing accounts.
* There are `@alice.test` and `@bob.test` now.
* @param handle - handle name for test agent (without `@`)
*/
export async function getTestAgent(handle: Handle) {
return testAgents[handle];
}
3 changes: 2 additions & 1 deletion packages/client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"moduleResolution": "bundler",
"paths": {
"~/*": ["src/*"],
"~~/*": ["*"],
"@tsky/lexicons": ["../lexicons/index.ts"],
},
"resolveJsonModule": true,
Expand All @@ -31,7 +32,7 @@
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": ["./src/**/*", "./tests/**/*", "./test-utils/**/*"],
"include": ["./src/**/*", "./globalSetup.ts", "./test-utils.ts"],
"typedocOptions": {
"out": "./docs",
"theme": "default",
Expand Down
2 changes: 2 additions & 0 deletions packages/client/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globalSetup: 'globalSetup.ts',
coverage: {
provider: 'istanbul',
reporter: ['text', 'json-summary', 'json', 'html'],
Expand All @@ -11,6 +12,7 @@ export default defineConfig({
resolve: {
alias: {
'~': '/src',
'~~': '/',
},
},
});
2 changes: 2 additions & 0 deletions packages/internal/dev-env/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ADMIN_PASSWORD = 'admin-pass';
export const JWT_SECRET = 'jwt-secret';
5 changes: 5 additions & 0 deletions packages/internal/dev-env/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './constants.js';
export * from './network.js';
export * from './pds.js';
export * from './plc.js';
export * from './utils.js';
32 changes: 32 additions & 0 deletions packages/internal/dev-env/lib/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TestPdsServer, type PdsServerOptions } from './pds.js';
import { TestPlcServer, type PlcServerOptions } from './plc.js';
import { mockNetworkUtilities } from './utils.js';

export type NetworkConfig = {
pds: Partial<PdsServerOptions>;
plc: Partial<PlcServerOptions>;
};

export class TestNetwork {
constructor(
public readonly plc: TestPlcServer,
public readonly pds: TestPdsServer,
) {}

static async create(cfg: Partial<NetworkConfig>): Promise<TestNetwork> {
const plc = await TestPlcServer.create(cfg.plc ?? {});
const pds = await TestPdsServer.create({ didPlcUrl: plc.url, ...cfg.pds });

mockNetworkUtilities(pds);

return new TestNetwork(plc, pds);
}

async processAll() {
await this.pds.processAll();
}

async close() {
await Promise.all([this.plc.close(), this.pds.close()]);
}
}
119 changes: 119 additions & 0 deletions packages/internal/dev-env/lib/pds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { Secp256k1Keypair, randomStr } from '@atproto/crypto';
import * as pds from '@atproto/pds';

import getPort from 'get-port';
import * as ui8 from 'uint8arrays';

import { ADMIN_PASSWORD, JWT_SECRET } from './constants.js';

export interface PdsServerOptions extends Partial<pds.ServerEnvironment> {
didPlcUrl: string;
}

export interface AdditionalPdsContext {
dataDirectory: string;
blobstoreLoc: string;
}

export class TestPdsServer {
constructor(
public readonly server: pds.PDS,
public readonly url: string,
public readonly port: number,
public readonly additional: AdditionalPdsContext,
) {}

static async create(config: PdsServerOptions): Promise<TestPdsServer> {
const plcRotationKey = await Secp256k1Keypair.create({ exportable: true });
const plcRotationPriv = ui8.toString(await plcRotationKey.export(), 'hex');
const recoveryKey = (await Secp256k1Keypair.create()).did();

const port = config.port || (await getPort());
const url = `http://localhost:${port}`;

const blobstoreLoc = path.join(os.tmpdir(), randomStr(8, 'base32'));
const dataDirectory = path.join(os.tmpdir(), randomStr(8, 'base32'));

await fs.mkdir(dataDirectory, { recursive: true });

const env: pds.ServerEnvironment = {
devMode: true,
port,
dataDirectory: dataDirectory,
blobstoreDiskLocation: blobstoreLoc,
recoveryDidKey: recoveryKey,
adminPassword: ADMIN_PASSWORD,
jwtSecret: JWT_SECRET,
serviceHandleDomains: ['.test'],
bskyAppViewUrl: 'https://appview.invalid',
bskyAppViewDid: 'did:example:invalid',
bskyAppViewCdnUrlPattern: 'http://cdn.appview.com/%s/%s/%s',
modServiceUrl: 'https://moderator.invalid',
modServiceDid: 'did:example:invalid',
plcRotationKeyK256PrivateKeyHex: plcRotationPriv,
inviteRequired: false,
disableSsrfProtection: true,
serviceName: 'Development PDS',
brandColor: '#ffcb1e',
errorColor: undefined,
logoUrl:
'https://uxwing.com/wp-content/themes/uxwing/download/animals-and-birds/bee-icon.png',
homeUrl: 'https://bsky.social/',
termsOfServiceUrl: 'https://bsky.social/about/support/tos',
privacyPolicyUrl: 'https://bsky.social/about/support/privacy-policy',
supportUrl: 'https://blueskyweb.zendesk.com/hc/en-us',
...config,
};

const cfg = pds.envToCfg(env);
const secrets = pds.envToSecrets(env);

const server = await pds.PDS.create(cfg, secrets);

await server.start();

return new TestPdsServer(server, url, port, {
dataDirectory: dataDirectory,
blobstoreLoc: blobstoreLoc,
});
}

get ctx(): pds.AppContext {
return this.server.ctx;
}

adminAuth(): string {
return `Basic ${ui8.toString(
ui8.fromString(`admin:${ADMIN_PASSWORD}`, 'utf8'),
'base64pad',
)}`;
}

adminAuthHeaders() {
return {
authorization: this.adminAuth(),
};
}

jwtSecretKey() {
return pds.createSecretKeyObject(JWT_SECRET);
}

async processAll() {
await this.ctx.backgroundQueue.processAll();
}

async close() {
await this.server.destroy();

await fs.rm(this.additional.dataDirectory, {
recursive: true,
force: true,
});
await fs.rm(this.additional.blobstoreLoc, { force: true });
}
}
35 changes: 35 additions & 0 deletions packages/internal/dev-env/lib/plc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type AppContext, Database, PlcServer } from '@did-plc/server';

import getPort from 'get-port';

export interface PlcServerOptions {
port?: number;
}

export class TestPlcServer {
constructor(
public readonly server: PlcServer,
public readonly url: string,
public readonly port: number,
) {}

static async create(cfg: PlcServerOptions = {}): Promise<TestPlcServer> {
const port = cfg.port ?? (await getPort());
const url = `http://localhost:${port}`;

const db = Database.mock();
const server = PlcServer.create({ db, port });

await server.start();

return new TestPlcServer(server, url, port);
}

get context(): AppContext {
return this.server.ctx;
}

async close() {
await this.server.destroy();
}
}
Loading