Skip to content

Commit

Permalink
chore(test): Add cache layer to test tokens
Browse files Browse the repository at this point in the history
Add token cache layer

Add more tests

Add account tests

add more tests
  • Loading branch information
neet committed Apr 8, 2023
1 parent 8b4b46f commit df5a307
Show file tree
Hide file tree
Showing 28 changed files with 548 additions and 128 deletions.
5 changes: 3 additions & 2 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"shortcode",
"subprotocol",
"subresource",
"Tootcli",
"tootctl",
"trendable",
"typedoc",
"unassign",
Expand All @@ -48,6 +48,7 @@
"unprocessable",
"unreblog",
"unreviewed",
"unsilence"
"unsilence",
"unstorage"
]
}
8 changes: 6 additions & 2 deletions src/mastodon/v1/repositories/list-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ export class ListRepository
*/
@version({ since: '2.1.0' })
addAccount(id: string, params: AddListAccountsParams): Promise<void> {
return this.http.post<void>(`/api/v1/lists/${id}/accounts`, params);
return this.http.post<void>(`/api/v1/lists/${id}/accounts`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}

/**
Expand All @@ -119,6 +121,8 @@ export class ListRepository
*/
@version({ since: '2.1.0' })
removeAccount(id: string, params: RemoveListAccountsParams): Promise<void> {
return this.http.delete<void>(`/api/v1/lists/${id}/accounts`, params);
return this.http.delete<void>(`/api/v1/lists/${id}/accounts`, params, {
headers: { 'Content-Type': 'multipart/form-data' },
});
}
}
40 changes: 40 additions & 0 deletions test-utils/cache/cache-fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'node:fs';
import path from 'node:path';

import type { Cache } from './cache';

export class CacheFs<T> implements Cache<T> {
private readonly dir = 'node_modules/.cache/tokens';

set(value: T): void {
if (!fs.existsSync(this.dir)) {
fs.mkdirSync(this.dir, { recursive: true });
}

fs.writeFileSync(
path.join(this.dir, `${Date.now()}.json`),
JSON.stringify(value),
);
}

getAll(): T[] {
if (!fs.existsSync(this.dir)) {
fs.mkdirSync(this.dir, { recursive: true });
}

const files = fs
.readdirSync(this.dir)
.filter((file) => file.endsWith('.json'))
.map((file) => path.join(this.dir, file));

const entries = [];

for (const file of files) {
const entry = fs.readFileSync(file, 'utf8');
const parsed = JSON.parse(entry);
entries.push(parsed);
}

return entries;
}
}
4 changes: 4 additions & 0 deletions test-utils/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Cache<T> {
set(value: T): void;
getAll(): T[];
}
2 changes: 2 additions & 0 deletions test-utils/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './cache';
export * from './cache-fs';
8 changes: 4 additions & 4 deletions test-utils/jest-global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export default async (): Promise<void> => {
scopes: 'read write follow push admin:read admin:write',
});

const container = process.env.MASTODON_CONTAINER;
if (container == undefined) {
throw new Error('MASTODON_CONTAINER is not defined');
}
const container = process.env.MASTODON_CONTAINER ?? 'mastodon';
// if (container == undefined) {
// throw new Error('MASTODON_CONTAINER is not defined');
// }

const tokenPool = new TokenPoolImpl(container, masto, app);

Expand Down
1 change: 0 additions & 1 deletion test-utils/jest-setup-after-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ globalThis.admin = createClient({
version: __misc__.instance.version,
streamingApiUrl: __misc__.instance.urls.streamingApi,
accessToken: __misc__.adminToken.accessToken,
logLevel: 'info',
});

globalThis.clients = new ClientPoolImpl();
2 changes: 1 addition & 1 deletion test-utils/pools/client-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class ClientPoolImpl implements ClientPool {
version: __misc__.instance.version,
streamingApiUrl: __misc__.instance.urls.streamingApi,
accessToken: token.accessToken,
logLevel: 'info',
logLevel: 'debug',
});

this.clientToToken.set(client, token);
Expand Down
20 changes: 15 additions & 5 deletions test-utils/pools/token-pool.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import crypto from 'node:crypto';

import type { Pool } from 'generic-pool';
import { createPool } from 'generic-pool';

import type { mastodon } from '../../src';
import type { Tootcli } from '../tootcli';
import { createTootcli } from '../tootcli';
import { CacheFs } from '../cache';
import type { Tootctl } from '../tootctl';
import { createTootctl } from '../tootctl';

export interface TokenPool {
acquire(): Promise<mastodon.v1.Token>;
release(token: mastodon.v1.Token): Promise<void>;
}

export class TokenPoolImpl implements TokenPool {
private readonly tootcli: Tootcli;
private readonly tootctl: Tootctl;
private readonly pool: Pool<mastodon.v1.Token>;
private readonly cache = new CacheFs<mastodon.v1.Token>();
private readonly tokens = this.cache.getAll();

constructor(
container: string,
private readonly client: mastodon.Client,
private readonly app: mastodon.v1.Client,
) {
this.tootcli = createTootcli({ container });
this.tootctl = createTootctl({ container });
this.pool = createPool(
{
create: async () => {
const token = this.tokens.shift();
if (token != undefined) {
return token;
}
return this.create();
},
destroy: async () => {
Expand All @@ -48,7 +56,7 @@ export class TokenPoolImpl implements TokenPool {
const username = crypto.randomBytes(8).toString('hex');
const email = crypto.randomBytes(8).toString('hex') + '@example.com';

const { password } = await this.tootcli.accounts.create(username, {
const { password } = await this.tootctl.accounts.create(username, {
email,
confirmed: true,
});
Expand All @@ -62,6 +70,8 @@ export class TokenPoolImpl implements TokenPool {
scope: 'read write follow push admin:read admin:write',
});

this.cache.set(token);

return token;
};
}
2 changes: 0 additions & 2 deletions test-utils/tootcli/index.ts

This file was deleted.

2 changes: 2 additions & 0 deletions test-utils/tootctl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './tootctl';
export * from './tootctl-docker';
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import util from 'node:util';
import type {
CreateAccountParams,
CreateAccountResult,
Tootcli,
} from './tootcli';
Tootctl,
} from './tootctl';

const exec = util.promisify(childProcess.exec);

Expand All @@ -21,11 +21,11 @@ const stringifyArguments = (
.join(' ');
};

export type CreateTootcliParams = {
export type CreateTootctlParams = {
readonly container: string;
};

export const createTootcli = (params: CreateTootcliParams): Tootcli => {
export const createTootctl = (params: CreateTootctlParams): Tootctl => {
const { container } = params;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type CreateAccountResult = {
password: string;
};

export interface Tootcli {
export interface Tootctl {
accounts: {
create: (
username: string,
Expand Down
132 changes: 117 additions & 15 deletions tests/v1/accounts.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,6 @@ describe('account', () => {
});
});

// it('excludes replies from iterateStatuses', async () => {
// const statuses = await client.v1.accounts.listStatuses(TARGET_ID, {
// excludeReplies: true,
// });

// expect(
// statuses
// // `excludeReplies` won't exclude reblogs
// .filter((status) => !status.reblog)
// // `excludeReplies` won't exclude self-replies
// .filter((status) => status.inReplyToAccountId !== status.account.id)
// .every((status) => status.inReplyToId == undefined),
// ).toBe(true);
// });

it('fetches relationships', () => {
return clients.use(3, async ([alice, bob, carol]) => {
const bobId = await bob.v1.accounts
Expand All @@ -124,4 +109,121 @@ describe('account', () => {
expect(res).toHaveLength(2);
});
});

it('lists followers', () => {
return clients.use(2, async ([alice, bob]) => {
const aliceId = await alice.v1.accounts
.verifyCredentials()
.then((me) => me.id);
const bobId = await bob.v1.accounts
.verifyCredentials()
.then((me) => me.id);

await alice.v1.accounts.follow(bobId);
const followers = await alice.v1.accounts.listFollowers(bobId);

expect(followers).toEqual(
expect.arrayContaining([expect.objectContaining({ id: aliceId })]),
);
await alice.v1.accounts.unfollow(bobId);
});
});

it('lists following', () => {
return clients.use(2, async ([alice, bob]) => {
const bobId = await bob.v1.accounts
.verifyCredentials()
.then((me) => me.id);
await alice.v1.accounts.follow(bobId);

const aliceId = await alice.v1.accounts
.verifyCredentials()
.then((me) => me.id);
const accounts = await alice.v1.accounts.listFollowing(aliceId);

expect(accounts).toEqual(
expect.arrayContaining([expect.objectContaining({ id: bobId })]),
);
await alice.v1.accounts.unfollow(bobId);
});
});

it('lists statuses', () => {
return clients.use(async (client) => {
const status = await client.v1.statuses.create({ status: 'Hello' });
const statuses = await client.v1.accounts.listStatuses(status.account.id);

expect(statuses).toEqual(
expect.arrayContaining([expect.objectContaining({ id: status.id })]),
);
});
});

it('searches', () => {
return clients.use(async (client) => {
const me = await client.v1.accounts.verifyCredentials();
const accounts = await client.v1.accounts.search({ q: me.username });

expect(accounts).toEqual(
expect.arrayContaining([expect.objectContaining({ id: me.id })]),
);
});
});

test.todo('list lists');

it('lists featured tags', () => {
return clients.use(async (client) => {
const me = await client.v1.accounts.verifyCredentials();
const featuredTag = await client.v1.featuredTags.create({
name: 'masto',
});

const tags = await client.v1.accounts.listFeaturedTags(me.id);
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: featuredTag.id }),
]),
);

await client.v1.featuredTags.remove(featuredTag.id);
});
});

it('lists Identity proofs', () => {
return clients.use(async (client) => {
const me = await client.v1.accounts.verifyCredentials();
const identityProofs = await client.v1.accounts.listIdentityProofs(me.id);
expect(identityProofs).toEqual(expect.any(Array));
});
});

it('fetches familiar followers', () => {
return clients.use(async (client) => {
const me = await client.v1.accounts.verifyCredentials();
const identityProofs = await client.v1.accounts.fetchFamiliarFollowers([
me.id,
]);
expect(identityProofs).toEqual(expect.any(Array));
});
});

test.todo('lookup');

it('removes from followers', () => {
return clients.use(2, async ([alice, bob]) => {
const aliceId = await alice.v1.accounts
.verifyCredentials()
.then((me) => me.id);
const bobId = await bob.v1.accounts
.verifyCredentials()
.then((me) => me.id);

await bob.v1.accounts.follow(aliceId);
await alice.v1.accounts.removeFromFollowers(bobId);

const [rel] = await alice.v1.accounts.fetchRelationships([bobId]);
expect(rel.followedBy).toBe(false);
});
});
});
6 changes: 6 additions & 0 deletions tests/v1/announcements.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
describe('announcements', () => {
test.todo('lists announcements');
test.todo('dismisses announcement');
test.todo('adds a reaction');
test.todo('removes a reaction');
});
20 changes: 20 additions & 0 deletions tests/v1/apps.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
describe('apps', () => {
it('creates an app', () => {
return clients.use(async (client) => {
const app = await client.v1.apps.create({
clientName: 'My App',
redirectUris: 'https://example.com/oauth/callback',
scopes: 'read write',
});

expect(app.name).toBe('My App');
});
});

it('verifies an app', () => {
return clients.use(async (client) => {
const app = await client.v1.apps.verifyCredentials();
expect(app.name).toEqual(expect.any(String));
});
});
});

0 comments on commit df5a307

Please sign in to comment.