feat: biome check
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export const config = {
|
||||
mongodb: "mongo:8.0.3",
|
||||
postgres: "postgres:17",
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
import { getAvailablePort } from "@std/net";
|
||||
import cookie from "cookie";
|
||||
|
||||
import { auth, Session } from "~libraries/auth/mod.ts";
|
||||
import { Code } from "~libraries/code/aggregates/code.ts";
|
||||
import { handler } from "~libraries/server/handler.ts";
|
||||
|
||||
import { Api, QueryMethod } from "../.generated/api.ts";
|
||||
|
||||
export class ApiTestContainer {
|
||||
#server?: Deno.HttpServer;
|
||||
#client?: Api;
|
||||
#cookie?: string;
|
||||
#session?: Session;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Accessors
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
get accountId(): string | undefined {
|
||||
if (this.#session?.valid === true) {
|
||||
return this.#session.accountId;
|
||||
}
|
||||
}
|
||||
|
||||
get client() {
|
||||
if (this.#client === undefined) {
|
||||
throw new Error("ApiContainer > .start() has not been executed.");
|
||||
}
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Lifecycle
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async start(): Promise<this> {
|
||||
const port = await getAvailablePort();
|
||||
this.#server = await Deno.serve({ port, hostname: "127.0.0.1" }, handler);
|
||||
this.#client = makeApiClient(port, {
|
||||
onBeforeRequest: (headers: Headers) => {
|
||||
if (this.#cookie !== undefined) {
|
||||
headers.set("cookie", this.#cookie);
|
||||
}
|
||||
},
|
||||
onAfterResponse: (response) => {
|
||||
const cookie = response.headers.get("set-cookie");
|
||||
if (cookie !== null) {
|
||||
this.#cookie = cookie;
|
||||
}
|
||||
},
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.#server?.shutdown();
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Utilities
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async authorize(accountId: string): Promise<void> {
|
||||
const code = await Code.create({ identity: { type: "admin", accountId } }).save();
|
||||
await this.client.auth.code(accountId, code.id, code.value, {});
|
||||
this.#session = await this.getSession();
|
||||
}
|
||||
|
||||
async getSession(): Promise<Session | undefined> {
|
||||
const token = cookie.parse(this.#cookie ?? "").token;
|
||||
if (token !== undefined) {
|
||||
const session = await auth.resolve(token);
|
||||
if (session.valid === true) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unauthorize(): void {
|
||||
this.#cookie = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function makeApiClient(
|
||||
port: number,
|
||||
{
|
||||
onBeforeRequest,
|
||||
onAfterResponse,
|
||||
}: {
|
||||
onBeforeRequest: (headers: Headers) => void;
|
||||
onAfterResponse: (response: Response) => void;
|
||||
},
|
||||
): Api {
|
||||
return new Api({
|
||||
async command(payload) {
|
||||
const headers = new Headers();
|
||||
onBeforeRequest(headers);
|
||||
headers.set("content-type", "application/json");
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/v1/command`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (response.status >= 300) {
|
||||
console.error(
|
||||
`Command '${payload.method}' responded with error status '${response.status} ${response.statusText}'.`,
|
||||
);
|
||||
}
|
||||
if (response.headers.get("content-type")?.includes("json") === true) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
},
|
||||
async query(method: QueryMethod, path: string, query: Record<string, unknown>, body: any = {}) {
|
||||
const headers = new Headers();
|
||||
onBeforeRequest(headers);
|
||||
if (method !== "GET") {
|
||||
headers.set("content-type", "application/json");
|
||||
}
|
||||
const response = await fetch(`http://127.0.0.1:${port}${path}${getSearchQuery(query)}`, {
|
||||
method,
|
||||
headers,
|
||||
body: method === "GET" ? undefined : JSON.stringify(body),
|
||||
});
|
||||
onAfterResponse(response);
|
||||
const text = await response.text();
|
||||
if (response.status >= 300) {
|
||||
console.error(`Query '${path}' responded with error status '${response.status} ${response.statusText}'.`);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
if (response.headers.get("content-type")?.includes("json") === true) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getSearchQuery(query: Record<string, unknown>): string {
|
||||
const search: string[] = [];
|
||||
for (const key in query) {
|
||||
search.push(`${key}=${query[key]}`);
|
||||
}
|
||||
if (search.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return `?${search.join("&")}`;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { MongoTestContainer } from "@valkyr/testcontainers/mongodb";
|
||||
|
||||
import { container } from "~database/container.ts";
|
||||
import { logger } from "~libraries/logger/mod.ts";
|
||||
import { bootstrap } from "~libraries/utilities/bootstrap.ts";
|
||||
import { API_DOMAINS_DIR, API_PACKAGES_DIR } from "~paths";
|
||||
|
||||
export class DatabaseTestContainer {
|
||||
constructor(readonly mongo: MongoTestContainer) {
|
||||
container.set("client", mongo.client);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Lifecycle
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async start(): Promise<this> {
|
||||
logger.prefix("Database").info("DatabaseTestContainer Started");
|
||||
|
||||
await bootstrap(API_DOMAINS_DIR);
|
||||
await bootstrap(API_PACKAGES_DIR);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
async truncate() {
|
||||
const promises: Promise<any>[] = [];
|
||||
for (const dbName of ["balto:auth", "balto:code", "balto:consultant", "balto:task"]) {
|
||||
const db = this.mongo.client.db(dbName);
|
||||
const collections = await db.listCollections().toArray();
|
||||
promises.push(...collections.map(({ name }) => db.collection(name).deleteMany({})));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
logger.prefix("Database").info("DatabaseTestContainer stopped");
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { MongoTestContainer } from "@valkyr/testcontainers/mongodb";
|
||||
|
||||
import { config } from "../config.ts";
|
||||
import { ApiTestContainer } from "./api-container.ts";
|
||||
import { DatabaseTestContainer } from "./database-container.ts";
|
||||
|
||||
export class TestContainer {
|
||||
readonly id = crypto.randomUUID();
|
||||
|
||||
// ### Enablers
|
||||
// A map of services to enable when the TestContainer is started. These toggles
|
||||
// must be toggled before the container is started.
|
||||
|
||||
#with: With = {
|
||||
mongodb: false,
|
||||
database: false,
|
||||
api: false,
|
||||
};
|
||||
|
||||
// ### Needs
|
||||
|
||||
#needs: Needs = {
|
||||
mongodb: [],
|
||||
database: ["mongodb"],
|
||||
api: ["mongodb", "database"],
|
||||
};
|
||||
|
||||
// ### Services
|
||||
// Any services that has been enabled will be running under the following
|
||||
// assignments. Make sure to .stop any running services to avoid shutdown
|
||||
// leaks.
|
||||
|
||||
#mongodb?: MongoTestContainer;
|
||||
#database?: DatabaseTestContainer;
|
||||
#api?: ApiTestContainer;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Accessors
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
get accountId() {
|
||||
if (this.#api === undefined) {
|
||||
throw new Error("TestContainer > .withApi() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#api.accountId;
|
||||
}
|
||||
|
||||
get mongodb(): MongoTestContainer {
|
||||
if (this.#mongodb === undefined) {
|
||||
throw new Error("TestContainer > .withMongo() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#mongodb;
|
||||
}
|
||||
|
||||
get database(): DatabaseTestContainer {
|
||||
if (this.#database === undefined) {
|
||||
throw new Error("TestContainer > .withDatabase() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#database;
|
||||
}
|
||||
|
||||
get api() {
|
||||
if (this.#api === undefined) {
|
||||
throw new Error("TestContainer > .withApi() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#api.client;
|
||||
}
|
||||
|
||||
get authorize() {
|
||||
if (this.#api === undefined) {
|
||||
throw new Error("TestContainer > .withApi() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#api.authorize.bind(this.#api);
|
||||
}
|
||||
|
||||
get unauthorize() {
|
||||
if (this.#api === undefined) {
|
||||
throw new Error("TestContainer > .withApi() must be called before starting the TestContainer.");
|
||||
}
|
||||
return this.#api.unauthorize.bind(this.#api);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Builder
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
withMongo(): this {
|
||||
this.#with.mongodb = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
withDatabase(): this {
|
||||
this.#with.database = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
withApi(): this {
|
||||
this.#with.api = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Lifecycle
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
async start(): Promise<this> {
|
||||
const promises: Promise<void>[] = [];
|
||||
if (this.#isNeeded("mongodb") === true) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
this.#mongodb = await MongoTestContainer.start(config.mongodb);
|
||||
if (this.#isNeeded("database") === true) {
|
||||
this.#database = await new DatabaseTestContainer(this.mongodb).start();
|
||||
}
|
||||
})(),
|
||||
);
|
||||
}
|
||||
if (this.#isNeeded("api") === true) {
|
||||
promises.push(
|
||||
(async () => {
|
||||
this.#api = await new ApiTestContainer().start();
|
||||
})(),
|
||||
);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
return this;
|
||||
}
|
||||
|
||||
async stop(): Promise<this> {
|
||||
await this.#api?.stop();
|
||||
await this.#database?.stop();
|
||||
await this.#mongodb?.stop();
|
||||
|
||||
this.#api = undefined;
|
||||
this.#database = undefined;
|
||||
this.#mongodb = undefined;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#isNeeded(target: keyof With): boolean {
|
||||
if (this.#with[target] !== false) {
|
||||
return true;
|
||||
}
|
||||
for (const key in this.#needs) {
|
||||
if (this.#with[key as keyof With] !== false && this.#needs[key as keyof With].includes(target) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
type Needs = Record<keyof With, (keyof With)[]>;
|
||||
|
||||
type With = {
|
||||
mongodb: boolean;
|
||||
database: boolean;
|
||||
api: boolean;
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as assertSuite from "@std/assert";
|
||||
import * as bddSuite from "@std/testing/bdd";
|
||||
|
||||
import type { TestContainer } from "~libraries/testing/containers/test-container.ts";
|
||||
|
||||
import { authorize } from "./utilities/account.ts";
|
||||
|
||||
export function describe(name: string, runner: TestRunner): (container: TestContainer) => void {
|
||||
return (container: TestContainer) =>
|
||||
bddSuite.describe(name, () => runner(container, bddSuite, assertSuite, { authorize: authorize(container) }));
|
||||
}
|
||||
|
||||
export type TestRunner = (
|
||||
container: TestContainer,
|
||||
bdd: {
|
||||
[key in keyof typeof bddSuite]: (typeof bddSuite)[key];
|
||||
},
|
||||
assert: {
|
||||
[key in keyof typeof assertSuite]: (typeof assertSuite)[key];
|
||||
},
|
||||
utils: {
|
||||
authorize: ReturnType<typeof authorize>;
|
||||
},
|
||||
) => void;
|
||||
@@ -1,68 +0,0 @@
|
||||
import type { EventData } from "@valkyr/event-store";
|
||||
|
||||
import { AccountCreated, AccountEmailAdded } from "~libraries/auth/.generated/events.ts";
|
||||
import { Account } from "~libraries/auth/aggregates/account.ts";
|
||||
import { Role } from "~libraries/auth/aggregates/role.ts";
|
||||
import type { TestContainer } from "~libraries/testing/containers/test-container.ts";
|
||||
|
||||
type AuthorizationOptions = {
|
||||
name?: { family?: string; given?: string };
|
||||
email?: Partial<EventData<AccountEmailAdded>>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a function which provides the ability to create a new account which
|
||||
* is authorized and ready to use for testing authorized requests.
|
||||
*
|
||||
* @param container - Container to authorize against.
|
||||
*/
|
||||
export function authorize(container: TestContainer): AuthorizeFn {
|
||||
return async (data: EventData<AccountCreated>, { name = {}, email = {} }: AuthorizationOptions = {}) => {
|
||||
const role = await makeRole(data.type).save();
|
||||
const account = await Account.create(data, "test")
|
||||
.addName(name?.family ?? "Doe", name?.given ?? "John", "test")
|
||||
.addEmail({ value: "john.doe@fixture.none", type: "work", primary: true, verified: true, ...email }, "test")
|
||||
.addRole(role.id, "test")
|
||||
.save();
|
||||
await container.authorize(account.id);
|
||||
return account;
|
||||
};
|
||||
}
|
||||
|
||||
function makeRole(type: "admin" | "consultant" | "organization"): Role {
|
||||
switch (type) {
|
||||
case "admin": {
|
||||
return Role.create(
|
||||
{
|
||||
name: "Admin",
|
||||
permissions: [
|
||||
{ resource: "admin", actions: ["create", "update", "delete"] },
|
||||
{ resource: "consultant", actions: ["create", "update", "delete"] },
|
||||
{ resource: "organization", actions: ["create", "update", "delete"] },
|
||||
],
|
||||
},
|
||||
"test",
|
||||
);
|
||||
}
|
||||
case "consultant": {
|
||||
return Role.create(
|
||||
{
|
||||
name: "Consultant",
|
||||
permissions: [{ resource: "consultant", actions: ["create", "update", "delete"] }],
|
||||
},
|
||||
"test",
|
||||
);
|
||||
}
|
||||
case "organization": {
|
||||
return Role.create(
|
||||
{
|
||||
name: "Organization",
|
||||
permissions: [{ resource: "organization", actions: ["create", "update", "delete"] }],
|
||||
},
|
||||
"test",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AuthorizeFn = (data: EventData<AccountCreated>, optional?: AuthorizationOptions) => Promise<Account>;
|
||||
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Removes excess indentation caused by using multiline template strings.
|
||||
*
|
||||
* Ported from `dedent-js` solution.
|
||||
*
|
||||
* @see https://github.com/MartinKolarik/dedent-js
|
||||
*
|
||||
* @param templateStrings - Template strings to dedent.
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* nested: {
|
||||
* examples: [
|
||||
* dedent(`
|
||||
* I am 8 spaces off from the beginning of this file.
|
||||
* But I will be 2 spaces based on the trimmed distance
|
||||
* of the first line.
|
||||
* `),
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function dedent(templateStrings: TemplateStringsArray | string, ...values: any[]) {
|
||||
const matches = [];
|
||||
const strings = typeof templateStrings === "string" ? [templateStrings] : templateStrings.slice();
|
||||
|
||||
// Remove trailing whitespace.
|
||||
|
||||
strings[strings.length - 1] = strings[strings.length - 1].replace(/\r?\n([\t ]*)$/, "");
|
||||
|
||||
// Find all line breaks to determine the highest common indentation level.
|
||||
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
const match = strings[i].match(/\n[\t ]+/g);
|
||||
if (match) {
|
||||
matches.push(...match);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the common indentation from all strings.
|
||||
|
||||
if (matches.length) {
|
||||
const size = Math.min(...matches.map((value) => value.length - 1));
|
||||
const pattern = new RegExp(`\n[\t ]{${size}}`, "g");
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
strings[i] = strings[i].replace(pattern, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading whitespace.
|
||||
|
||||
strings[0] = strings[0].replace(/^\r?\n/, "");
|
||||
|
||||
// Perform interpolation.
|
||||
|
||||
let string = strings[0];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
string += values[i] + strings[i + 1];
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Traverse path and look for a `generate.ts` file in each folder found under
|
||||
* the given path. If a `generate.ts` file is found it is imported so its content
|
||||
* is executed.
|
||||
*
|
||||
* @param path - Path to resolve `generate.ts` files.
|
||||
* @param filter - Which folders found under the given path to ignore.
|
||||
*/
|
||||
export async function generate(path: string, filter: string[] = []): Promise<void> {
|
||||
const generate: string[] = [];
|
||||
for await (const entry of Deno.readDir(path)) {
|
||||
if (entry.isDirectory === true) {
|
||||
const moduleName = path.split("/").pop();
|
||||
if (moduleName === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (filter.length > 0 && filter.includes(moduleName) === false) {
|
||||
continue;
|
||||
}
|
||||
const filePath = `${path}/${entry.name}/.tasks/generate.ts`;
|
||||
if (await hasFile(filePath)) {
|
||||
generate.push(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const filePath of generate) {
|
||||
await import(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
async function hasFile(filePath: string) {
|
||||
try {
|
||||
await Deno.lstat(filePath);
|
||||
} catch (err) {
|
||||
if (!(err instanceof Deno.errors.NotFound)) {
|
||||
throw err;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -8,4 +8,4 @@
|
||||
"@module/workspace": "workspace:*",
|
||||
"zod": "4.1.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { procedure } from "@platform/relay";
|
||||
import z from "zod";
|
||||
|
||||
const EventSchema = z.object({
|
||||
id: z.uuid(),
|
||||
stream: z.uuid(),
|
||||
type: z.string(),
|
||||
data: z.any(),
|
||||
meta: z.any(),
|
||||
recorded: z.string(),
|
||||
created: z.string(),
|
||||
});
|
||||
|
||||
export default procedure
|
||||
.method("event")
|
||||
.access("public")
|
||||
.params(EventSchema)
|
||||
.response(z.uuid())
|
||||
.handle(async (event) => {
|
||||
console.log(event);
|
||||
return crypto.randomUUID();
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import socket from "@platform/socket/server.ts";
|
||||
import { storage } from "@platform/storage";
|
||||
|
||||
import { config } from "./config.ts";
|
||||
import session from "./services/session.ts";
|
||||
import session from "./session.ts";
|
||||
|
||||
const log = logger.prefix("Server");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user