-
-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(test): Add E2E tests with multiple accounts
Use --name option to reuse existing container Move generic pool to global setup improve account API fix fix parallel
- Loading branch information
Showing
14 changed files
with
383 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,6 +37,7 @@ | |
"shortcode", | ||
"subprotocol", | ||
"subresource", | ||
"Tootcli", | ||
"trendable", | ||
"typedoc", | ||
"unassign", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,16 @@ | ||
/* eslint-disable no-var */ | ||
import type { mastodon } from '../src'; | ||
import type { ClientPool, TokenPool } from './pools'; | ||
|
||
declare global { | ||
var url: string; | ||
var token: mastodon.v1.Token; | ||
var instance: mastodon.v1.Instance; | ||
var admin: mastodon.Client; | ||
var clients: ClientPool; | ||
|
||
/** Should only be used inside /test-utils */ | ||
var __misc__: { | ||
url: string; | ||
instance: mastodon.v1.Instance; | ||
adminToken: mastodon.v1.Token; | ||
tokens: TokenPool; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,38 @@ | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
import type { mastodon } from '../src'; | ||
import { fetchV1Instance, login } from '../src'; | ||
import { TokenPoolImpl } from './pools'; | ||
|
||
const BASE_URL = 'http://localhost:3000'; | ||
export default async (): Promise<void> => { | ||
const url = 'http://localhost:3000'; | ||
const instance = await fetchV1Instance({ url }); | ||
const masto = await login({ url }); | ||
|
||
const createApp = async (): Promise<mastodon.v1.Application> => { | ||
const masto = await login({ url: BASE_URL }); | ||
const app = await masto.v1.apps.create({ | ||
clientName: 'Masto.js', | ||
redirectUris: 'urn:ietf:wg:oauth:2.0:oob', | ||
scopes: 'read write follow push admin:read admin:write', | ||
}); | ||
return app; | ||
}; | ||
|
||
const createToken = async ( | ||
app: mastodon.v1.Client, | ||
): Promise<mastodon.v1.Token> => { | ||
const masto = await login({ url: BASE_URL }); | ||
const token = await masto.oauth.createToken({ | ||
const container = process.env.MASTODON_CONTAINER; | ||
if (container == undefined) { | ||
throw new Error('MASTODON_CONTAINER is not defined'); | ||
} | ||
|
||
const tokenPool = new TokenPoolImpl(container, masto, app); | ||
|
||
const adminToken = await masto.oauth.createToken({ | ||
grantType: 'password', | ||
clientId: app.clientId!, | ||
clientSecret: app.clientSecret!, | ||
username: 'admin@localhost:3000', | ||
password: 'mastodonadmin', | ||
scope: 'read write follow push admin:read admin:write', | ||
}); | ||
return token; | ||
}; | ||
|
||
export default async (): Promise<void> => { | ||
const app = await createApp(); | ||
globalThis.url = BASE_URL; | ||
globalThis.token = await createToken(app); | ||
globalThis.instance = await fetchV1Instance({ url: BASE_URL }); | ||
globalThis.__misc__ = { | ||
url, | ||
instance, | ||
tokens: tokenPool, | ||
adminToken, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,14 @@ | ||
import { createClient } from '../src'; | ||
import { ClientPoolImpl } from './pools'; | ||
|
||
jest.setTimeout(1000 * 60); | ||
|
||
const { url, instance, token } = globalThis; | ||
|
||
globalThis.admin = createClient({ | ||
url: url, | ||
version: instance.version, | ||
streamingApiUrl: instance.urls.streamingApi, | ||
accessToken: token.accessToken, | ||
url: __misc__.url, | ||
version: __misc__.instance.version, | ||
streamingApiUrl: __misc__.instance.urls.streamingApi, | ||
accessToken: __misc__.adminToken.accessToken, | ||
logLevel: 'info', | ||
}); | ||
|
||
globalThis.clients = new ClientPoolImpl(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import type { mastodon } from '../../src'; | ||
import { createClient } from '../../src'; | ||
|
||
type UseFn<T> = (client: mastodon.Client) => Promise<T>; | ||
type UseFnMany<T> = (client: mastodon.Client[]) => Promise<T>; | ||
|
||
export type ClientPool = { | ||
acquire(n?: 1 | undefined): Promise<mastodon.Client>; | ||
acquire(n: number): Promise<mastodon.Client[]>; | ||
|
||
release(token: mastodon.Client): Promise<void>; | ||
release(tokens: mastodon.Client[]): Promise<void>; | ||
|
||
use<T>(fn: UseFn<T>): Promise<T>; | ||
use<T>(n: number, fn: UseFnMany<T>): Promise<T>; | ||
}; | ||
|
||
export class ClientPoolImpl implements ClientPool { | ||
private readonly clientToToken = new WeakMap< | ||
mastodon.Client, | ||
mastodon.v1.Token | ||
>(); | ||
|
||
async acquire(n?: 1 | undefined): Promise<mastodon.Client>; | ||
async acquire(n: number): Promise<mastodon.Client[]>; | ||
async acquire(n = 1): Promise<mastodon.Client | mastodon.Client[]> { | ||
if (n === 1) { | ||
return this.acquireClient(); | ||
} | ||
|
||
return Promise.all( | ||
Array.from({ length: n }).map(() => this.acquireClient()), | ||
); | ||
} | ||
|
||
async release(client: mastodon.Client): Promise<void>; | ||
async release(clients: mastodon.Client[]): Promise<void>; | ||
async release(clients: mastodon.Client | mastodon.Client[]): Promise<void> { | ||
await (Array.isArray(clients) | ||
? Promise.all(clients.map((client) => this.releaseClient(client))) | ||
: this.releaseClient(clients)); | ||
} | ||
|
||
async use<T>(fn: UseFn<T>): Promise<T>; | ||
async use<T>(n: number, fn: UseFnMany<T>): Promise<T>; | ||
async use<T>( | ||
fnOrNumber: number | UseFn<T>, | ||
fnOrUndefined?: UseFnMany<T>, | ||
): Promise<T> { | ||
if (typeof fnOrNumber === 'function' && fnOrUndefined == undefined) { | ||
const fn = fnOrNumber; | ||
const client = await this.acquire(1); | ||
|
||
try { | ||
return await fn(client); | ||
} finally { | ||
await this.release(client); | ||
} | ||
} | ||
|
||
if (typeof fnOrNumber === 'number' && typeof fnOrUndefined === 'function') { | ||
const n = fnOrNumber; | ||
const fn = fnOrUndefined; | ||
const clients = await this.acquire(n); | ||
|
||
try { | ||
return await fn(clients); | ||
} finally { | ||
await this.release(clients); | ||
} | ||
} | ||
|
||
throw new Error('Invalid arguments'); | ||
} | ||
|
||
private acquireClient = async () => { | ||
const token = await __misc__.tokens.acquire(); | ||
|
||
const client = createClient({ | ||
url: __misc__.url, | ||
version: __misc__.instance.version, | ||
streamingApiUrl: __misc__.instance.urls.streamingApi, | ||
accessToken: token.accessToken, | ||
logLevel: 'info', | ||
}); | ||
|
||
this.clientToToken.set(client, token); | ||
return client; | ||
}; | ||
|
||
private releaseClient = async (client: mastodon.Client) => { | ||
const token = this.clientToToken.get(client); | ||
if (token == undefined) { | ||
return; | ||
} | ||
|
||
await globalThis.__misc__.tokens.release(token); | ||
this.clientToToken.delete(client); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './client-pool'; | ||
export * from './token-pool'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* 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'; | ||
|
||
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 pool: Pool<mastodon.v1.Token>; | ||
|
||
constructor( | ||
container: string, | ||
private readonly client: mastodon.Client, | ||
private readonly app: mastodon.v1.Client, | ||
) { | ||
this.tootcli = createTootcli({ container }); | ||
this.pool = createPool( | ||
{ | ||
create: async () => { | ||
return this.create(); | ||
}, | ||
destroy: async () => { | ||
return; | ||
}, | ||
}, | ||
{ max: 10 }, | ||
); | ||
} | ||
|
||
acquire = (): Promise<mastodon.v1.Token> => { | ||
return this.pool.acquire(); | ||
}; | ||
|
||
release = (token: mastodon.v1.Token): Promise<void> => { | ||
return this.pool.release(token); | ||
}; | ||
|
||
private readonly create = async (): Promise<mastodon.v1.Token> => { | ||
const username = crypto.randomBytes(8).toString('hex'); | ||
const email = crypto.randomBytes(8).toString('hex') + '@example.com'; | ||
|
||
const { password } = await this.tootcli.accounts.create(username, { | ||
email, | ||
confirmed: true, | ||
}); | ||
|
||
const token = await this.client.oauth.createToken({ | ||
grantType: 'password', | ||
clientId: this.app.clientId!, | ||
clientSecret: this.app.clientSecret!, | ||
username: email, | ||
password, | ||
scope: 'read write follow push admin:read admin:write', | ||
}); | ||
|
||
return token; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './tootcli'; | ||
export * from './tootcli-docker'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import childProcess from 'node:child_process'; | ||
import util from 'node:util'; | ||
|
||
import type { | ||
CreateAccountParams, | ||
CreateAccountResult, | ||
Tootcli, | ||
} from './tootcli'; | ||
|
||
const exec = util.promisify(childProcess.exec); | ||
|
||
const extractPassword = (stdout: string) => { | ||
return stdout.match(/New password:\s(.+?)$/)?.[1]; | ||
}; | ||
|
||
const stringifyArguments = ( | ||
args: Record<string, string | number | boolean>, | ||
): string => { | ||
return Object.entries(args) | ||
.map(([key, value]) => (value === true ? `--${key}` : `--${key}=${value}`)) | ||
.join(' '); | ||
}; | ||
|
||
export type CreateTootcliParams = { | ||
readonly container: string; | ||
}; | ||
|
||
export const createTootcli = (params: CreateTootcliParams): Tootcli => { | ||
const { container } = params; | ||
|
||
return { | ||
accounts: { | ||
create: async ( | ||
username: string, | ||
params: CreateAccountParams, | ||
): Promise<CreateAccountResult> => { | ||
const args = stringifyArguments(params); | ||
|
||
const { stdout } = await exec( | ||
[ | ||
`docker exec ${container}`, | ||
`bash -c "RAILS_ENV=development bin/tootctl accounts create ${username} ${args}"`, | ||
].join(' '), | ||
); | ||
|
||
const password = extractPassword(stdout.trim()); | ||
if (password == undefined) { | ||
throw new Error("Couldn't extract password from stdout"); | ||
} | ||
|
||
return { password }; | ||
}, | ||
}, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
export type CreateAccountParams = { | ||
readonly email: string; | ||
readonly confirmed: boolean; | ||
}; | ||
|
||
export type CreateAccountResult = { | ||
password: string; | ||
}; | ||
|
||
export interface Tootcli { | ||
accounts: { | ||
create: ( | ||
username: string, | ||
params: CreateAccountParams, | ||
) => Promise<CreateAccountResult>; | ||
}; | ||
} |
Oops, something went wrong.