Cobre rotas HTTP, proxy, worker assíncrono e barreiras do dev auth
Os 34 testes existentes batiam só na camada de serviço: as rotas, o proxy e o loop do worker não tinham cobertura alguma. A suíte vai de 34 para 102. - `tests/http/`: rotas reais com o caminho real de autenticação, sem subir servidor. Cobre 401 sem sessão, módulo desligado, papel sem permissão, 403 ao tentar gravar workspace de terceiro, e 429 com Retry-After. - `tests/http/proxy.test.ts`: o proxy não propaga query da requisição para o login, o que impediria um returnTo forjado. - `tests/integration/worker.test.ts`: jornada assíncrona pelo loop real. A web enfileira e devolve `queued`, o download falha antes do tick, o tick conclui e os três formatos saem. Também: job sobrevive à perda da fila do Redis, worker relê o contexto e não processa com a flag desligada, TTL expira temporários sem tocar o modelo, e desligamento gracioso. - `tests/unit/dev-auth-guard.test.ts`: matriz de ambientes das três barreiras do login local. Um token de dev válido é recusado quando o ambiente é staging. Espaço em volta de um valor de ADAPTER_ENV é tolerado de propósito, e isso está declarado no teste; valor desconhecido nunca vira development. - `tests/services/`: Postgres e Redis reais. Confere `app_user` sem BYPASSRLS, FORCE RLS em todas as tabelas, isolamento entre workspaces, auditoria não alterável pelo papel da aplicação, e lock entre conexões diferentes. Sem as variáveis de serviço os casos são IGNORADOS, nunca aprovados, e ficam fora de `npm test` para a suíte padrão passar em qualquer máquina. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dea510c3b4
commit
ce11a50091
10 files changed
+1182
No files matched your search
@@ -0,0 +1,187 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { adapterEnv, collectConfigIssues, stagingAuthEnabled } from "@/config/env";
|
||||
import { devAuthEnabled } from "@/server/auth/dev-session";
|
||||
|
||||
/**
|
||||
* O login local sem senha não pode existir em homologação pública, em nenhuma
|
||||
* combinação de variáveis. Há três barreiras independentes e esta suíte cobre
|
||||
* as três, na matriz inteira de ambientes.
|
||||
*/
|
||||
|
||||
const original = {
|
||||
ADAPTER_ENV: process.env.ADAPTER_ENV,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
ADAPTER_DEV_AUTH: process.env.ADAPTER_DEV_AUTH,
|
||||
ADAPTER_DEV_AUTH_SECRET: process.env.ADAPTER_DEV_AUTH_SECRET,
|
||||
};
|
||||
|
||||
function setEnv(values: Record<string, string | undefined>): void {
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
setEnv(original);
|
||||
});
|
||||
|
||||
describe("barreira 1: devAuthEnabled", () => {
|
||||
const casos: { nome: string; env: Record<string, string | undefined>; esperado: boolean }[] = [
|
||||
{
|
||||
nome: "development com flag e segredo",
|
||||
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: true,
|
||||
},
|
||||
{
|
||||
nome: "development sem a flag",
|
||||
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "0", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "development sem segredo",
|
||||
env: { ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: undefined },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "staging com flag e segredo",
|
||||
env: { ADAPTER_ENV: "staging", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "production com flag e segredo",
|
||||
env: { ADAPTER_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "ADAPTER_ENV ausente com NODE_ENV=production",
|
||||
env: { ADAPTER_ENV: undefined, NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "ADAPTER_ENV com valor inválido",
|
||||
env: { ADAPTER_ENV: "homolog", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "ADAPTER_ENV com maiúsculas não é aceito como development",
|
||||
env: { ADAPTER_ENV: "Development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
{
|
||||
nome: "staging mesmo com NODE_ENV=development",
|
||||
env: { ADAPTER_ENV: "staging", NODE_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" },
|
||||
esperado: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const caso of casos) {
|
||||
it(`${caso.nome} → ${caso.esperado ? "ligado" : "desligado"}`, () => {
|
||||
setEnv({ ADAPTER_ENV: undefined, NODE_ENV: "test", ADAPTER_DEV_AUTH: undefined, ADAPTER_DEV_AUTH_SECRET: undefined });
|
||||
setEnv(caso.env);
|
||||
expect(devAuthEnabled()).toBe(caso.esperado);
|
||||
});
|
||||
}
|
||||
|
||||
it("um valor inesperado de ADAPTER_ENV nunca resolve para development", () => {
|
||||
for (const valor of ["prod", "hml", "homolog", "homologacao", "stage", "staging ", "DEV", "Development", "dev"]) {
|
||||
setEnv({ ADAPTER_ENV: valor, NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
||||
expect(adapterEnv(), `valor ${JSON.stringify(valor)}`).not.toBe("development");
|
||||
expect(devAuthEnabled(), `valor ${JSON.stringify(valor)}`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("espaço em volta de um valor válido é tolerado, de propósito", () => {
|
||||
// Espaço sobrando em arquivo de env é erro de digitação comum, e o valor
|
||||
// ainda é inequívoco. O que nunca acontece é um valor DESCONHECIDO virar
|
||||
// development: o caso acima cobre isso, e `ADAPTER_ENV` vazio cai em
|
||||
// NODE_ENV. Reconhecer " staging " como staging é o comportamento seguro.
|
||||
setEnv({ ADAPTER_ENV: " staging ", NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
||||
expect(adapterEnv()).toBe("staging");
|
||||
expect(devAuthEnabled()).toBe(false);
|
||||
|
||||
setEnv({ ADAPTER_ENV: " development ", NODE_ENV: "production", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
||||
expect(adapterEnv()).toBe("development");
|
||||
expect(devAuthEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("barreira 2: o start é recusado", () => {
|
||||
const base = {
|
||||
ADAPTER_ENV: "staging",
|
||||
NEXT_PUBLIC_SUPABASE_URL: "https://p.supabase.co",
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "anon",
|
||||
SUPABASE_SERVICE_ROLE_KEY: "service",
|
||||
SUPABASE_STORAGE_BUCKET: "bucket-hml",
|
||||
DATABASE_URL: "postgres://host/db",
|
||||
REDIS_URL: "redis://host:6379",
|
||||
SPREADSHEET_TEMP_TTL_SECONDS: "86400",
|
||||
ADAPTER_JOB_MODE: "worker",
|
||||
ADAPTER_APPLY_IDENTITY_SHIM: "1",
|
||||
ADAPTER_STAGING_AUTH: "1",
|
||||
};
|
||||
|
||||
it("staging com ADAPTER_DEV_AUTH=1 não passa na validação", () => {
|
||||
const issues = collectConfigIssues({ ...base, ADAPTER_DEV_AUTH: "1" });
|
||||
expect(issues.map((issue) => issue.variable)).toContain("ADAPTER_DEV_AUTH");
|
||||
});
|
||||
|
||||
it("production com ADAPTER_DEV_AUTH=1 não passa na validação", () => {
|
||||
const issues = collectConfigIssues({
|
||||
...base,
|
||||
ADAPTER_ENV: "production",
|
||||
ADAPTER_STAGING_AUTH: "0",
|
||||
SENTRY_DSN: "https://k@sentry.io/1",
|
||||
ELO_PRO_LOGIN_URL: "https://login.elo.example",
|
||||
ADAPTER_DEV_AUTH: "1",
|
||||
});
|
||||
expect(issues.map((issue) => issue.variable)).toContain("ADAPTER_DEV_AUTH");
|
||||
});
|
||||
|
||||
it("staging sem a flag de dev passa", () => {
|
||||
expect(collectConfigIssues({ ...base, ADAPTER_DEV_AUTH: "0" })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("barreira 3: o provedor não entra no caminho de código", () => {
|
||||
it("fora de development só o Supabase resolve identidade", async () => {
|
||||
setEnv({ ADAPTER_ENV: "staging", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
||||
const { activeProviders } = await import("@/server/auth/identity-provider");
|
||||
expect(activeProviders().map((provider) => provider.name)).toEqual(["supabase"]);
|
||||
});
|
||||
|
||||
it("em development o provedor local vem primeiro", async () => {
|
||||
setEnv({ ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "s" });
|
||||
const { activeProviders } = await import("@/server/auth/identity-provider");
|
||||
expect(activeProviders().map((provider) => provider.name)).toEqual(["dev", "supabase"]);
|
||||
});
|
||||
|
||||
it("o provedor local recusa o token mesmo se for chamado em staging", async () => {
|
||||
setEnv({ ADAPTER_ENV: "development", ADAPTER_DEV_AUTH: "1", ADAPTER_DEV_AUTH_SECRET: "segredo" });
|
||||
const { signDevSession } = await import("@/server/auth/dev-session");
|
||||
const token = signDevSession("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "11111111-1111-4111-8111-111111111111");
|
||||
|
||||
setEnv({ ADAPTER_ENV: "staging" });
|
||||
const { devIdentityProvider } = await import("@/server/auth/identity-provider");
|
||||
const jar = {
|
||||
get: (name: string) => (name === "adapter_dev_session" ? { name, value: token } : undefined),
|
||||
getAll: () => [{ name: "adapter_dev_session", value: token }],
|
||||
set: () => undefined,
|
||||
};
|
||||
expect(await devIdentityProvider.resolve(jar as never)).toBeNull();
|
||||
expect(devIdentityProvider.hadSessionHint(jar as never)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("login de homologação não vaza para produção", () => {
|
||||
it("stagingAuthEnabled é falso em development e production", () => {
|
||||
const base = {
|
||||
ADAPTER_STAGING_AUTH: "1",
|
||||
NEXT_PUBLIC_SUPABASE_URL: "https://p.supabase.co",
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "anon",
|
||||
};
|
||||
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "staging" })).toBe(true);
|
||||
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "production" })).toBe(false);
|
||||
expect(stagingAuthEnabled({ ...base, ADAPTER_ENV: "development" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
adapterEnv,
|
||||
assertDeployedEnv,
|
||||
collectConfigIssues,
|
||||
identityShimEnabled,
|
||||
isDeployedEnv,
|
||||
jobRunsInline,
|
||||
requireDatabaseUrl,
|
||||
resourceNamespace,
|
||||
stagingAuthEnabled,
|
||||
} from "@/config/env";
|
||||
import { isShimMigration } from "@/infra/db/migrate";
|
||||
import { resolveWorkspace, type Membership } from "@/server/auth/workspace";
|
||||
|
||||
const ALPHA = "11111111-1111-4111-8111-111111111111";
|
||||
const BETA = "22222222-2222-4222-8222-222222222222";
|
||||
|
||||
function membership(workspaceId: string, name: string): Membership {
|
||||
return { workspaceId, workspaceName: name, role: "owner", moduleEnabled: true };
|
||||
}
|
||||
|
||||
const stagingBase = {
|
||||
ADAPTER_ENV: "staging",
|
||||
NODE_ENV: "production",
|
||||
NEXT_PUBLIC_SUPABASE_URL: "https://projeto.supabase.co",
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY: "anon",
|
||||
SUPABASE_SERVICE_ROLE_KEY: "service",
|
||||
SUPABASE_STORAGE_BUCKET: "adaptador-planilhas-hml",
|
||||
DATABASE_URL: "postgres://host/db",
|
||||
REDIS_URL: "redis://host:6379",
|
||||
SPREADSHEET_TEMP_TTL_SECONDS: "86400",
|
||||
ADAPTER_JOB_MODE: "worker",
|
||||
ADAPTER_APPLY_IDENTITY_SHIM: "1",
|
||||
ADAPTER_STAGING_AUTH: "1",
|
||||
};
|
||||
|
||||
describe("ambiente lógico", () => {
|
||||
it("separa development, staging e production", () => {
|
||||
expect(adapterEnv({ ADAPTER_ENV: "staging", NODE_ENV: "production" })).toBe("staging");
|
||||
expect(adapterEnv({ NODE_ENV: "production" })).toBe("production");
|
||||
expect(adapterEnv({ NODE_ENV: "development" })).toBe("development");
|
||||
expect(isDeployedEnv({ ADAPTER_ENV: "staging" })).toBe(true);
|
||||
expect(isDeployedEnv({ ADAPTER_ENV: "development", NODE_ENV: "production" })).toBe(false);
|
||||
});
|
||||
|
||||
it("trata ADAPTER_ENV inválido como ambiente implantado e aponta o erro", () => {
|
||||
const env = { ADAPTER_ENV: "homolog", NODE_ENV: "production" };
|
||||
expect(isDeployedEnv(env)).toBe(true);
|
||||
expect(collectConfigIssues(env).map((issue) => issue.variable)).toContain("ADAPTER_ENV");
|
||||
});
|
||||
|
||||
it("namespeia recursos por aplicativo e ambiente", () => {
|
||||
expect(resourceNamespace({ ADAPTER_ENV: "staging" })).toBe("adaptador-planilhas:staging");
|
||||
expect(resourceNamespace({ ADAPTER_ENV: "production" })).toBe("adaptador-planilhas:production");
|
||||
expect(resourceNamespace({ ADAPTER_ENV: "staging", ADAPTER_NAMESPACE: "outro-app" })).toBe("outro-app:staging");
|
||||
});
|
||||
|
||||
it("não deixa homologação e produção compartilharem namespace", () => {
|
||||
expect(resourceNamespace({ ADAPTER_ENV: "staging" })).not.toBe(resourceNamespace({ ADAPTER_ENV: "production" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("configuração de homologação", () => {
|
||||
it("aceita o conjunto completo", () => {
|
||||
expect(collectConfigIssues(stagingBase)).toEqual([]);
|
||||
expect(() => assertDeployedEnv(stagingBase)).not.toThrow();
|
||||
});
|
||||
|
||||
it("recusa dev auth, inline e bucket público", () => {
|
||||
const names = collectConfigIssues({
|
||||
...stagingBase,
|
||||
ADAPTER_DEV_AUTH: "1",
|
||||
ADAPTER_JOB_MODE: "inline",
|
||||
SUPABASE_STORAGE_BUCKET: "public",
|
||||
}).map((issue) => issue.variable);
|
||||
expect(names).toContain("ADAPTER_DEV_AUTH");
|
||||
expect(names).toContain("ADAPTER_JOB_MODE");
|
||||
expect(names).toContain("SUPABASE_STORAGE_BUCKET");
|
||||
});
|
||||
|
||||
it("exige decisão explícita sobre o shim de identidade", () => {
|
||||
const env = { ...stagingBase } as Record<string, string | undefined>;
|
||||
delete env.ADAPTER_APPLY_IDENTITY_SHIM;
|
||||
expect(collectConfigIssues(env).map((issue) => issue.variable)).toContain("ADAPTER_APPLY_IDENTITY_SHIM");
|
||||
});
|
||||
|
||||
it("exige um caminho de login em homologação", () => {
|
||||
const env = { ...stagingBase, ADAPTER_STAGING_AUTH: "0" };
|
||||
expect(collectConfigIssues(env).map((issue) => issue.variable)).toContain("ADAPTER_STAGING_AUTH");
|
||||
expect(collectConfigIssues({ ...env, ELO_PRO_LOGIN_URL: "https://login.elo.example" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("não deixa o login próprio de homologação ir a produção", () => {
|
||||
const env = {
|
||||
...stagingBase,
|
||||
ADAPTER_ENV: "production",
|
||||
SENTRY_DSN: "https://k@sentry.io/1",
|
||||
ELO_PRO_LOGIN_URL: "https://login.elo.example",
|
||||
};
|
||||
expect(collectConfigIssues(env).map((issue) => issue.variable)).toContain("ADAPTER_STAGING_AUTH");
|
||||
});
|
||||
|
||||
it("exige Sentry e login central em produção, mas não bloqueia homologação por isso", () => {
|
||||
const production = { ...stagingBase, ADAPTER_ENV: "production", ADAPTER_STAGING_AUTH: "0" };
|
||||
const names = collectConfigIssues(production).map((issue) => issue.variable);
|
||||
expect(names).toContain("SENTRY_DSN");
|
||||
expect(names).toContain("ELO_PRO_LOGIN_URL");
|
||||
expect(collectConfigIssues({ ...stagingBase, SENTRY_DSN: undefined })).toEqual([]);
|
||||
});
|
||||
|
||||
it("não valida nada em development", () => {
|
||||
expect(collectConfigIssues({ NODE_ENV: "development" })).toEqual([]);
|
||||
expect(() => assertDeployedEnv({ NODE_ENV: "development" })).not.toThrow();
|
||||
});
|
||||
|
||||
it("recusa staging sem nenhuma variável e lista o que falta", () => {
|
||||
expect(() => assertDeployedEnv({ ADAPTER_ENV: "staging" })).toThrow(/DATABASE_URL/);
|
||||
expect(() => assertDeployedEnv({ ADAPTER_ENV: "staging" })).toThrow(/REDIS_URL/);
|
||||
});
|
||||
|
||||
it("login próprio de homologação só liga com Supabase configurado", () => {
|
||||
expect(stagingAuthEnabled(stagingBase)).toBe(true);
|
||||
expect(stagingAuthEnabled({ ...stagingBase, NEXT_PUBLIC_SUPABASE_URL: "" })).toBe(false);
|
||||
expect(stagingAuthEnabled({ ...stagingBase, ADAPTER_ENV: "production" })).toBe(false);
|
||||
expect(stagingAuthEnabled({ ...stagingBase, ADAPTER_ENV: "development" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("banco e migrations", () => {
|
||||
it("nunca cai em PGlite fora de development", () => {
|
||||
expect(() => requireDatabaseUrl({ ADAPTER_ENV: "staging" })).toThrow(/DATABASE_URL/);
|
||||
expect(() => requireDatabaseUrl({ ADAPTER_ENV: "production" })).toThrow(/DATABASE_URL/);
|
||||
expect(requireDatabaseUrl({ NODE_ENV: "development" })).toBeNull();
|
||||
expect(requireDatabaseUrl({ ADAPTER_ENV: "staging", DATABASE_URL: "postgres://h/d" })).toBe("postgres://h/d");
|
||||
});
|
||||
|
||||
it("o shim de identidade é decisão explícita em ambiente implantado", () => {
|
||||
expect(identityShimEnabled({ NODE_ENV: "development" })).toBe(true);
|
||||
expect(identityShimEnabled({ ADAPTER_ENV: "staging" })).toBe(false);
|
||||
expect(identityShimEnabled({ ADAPTER_ENV: "staging", ADAPTER_APPLY_IDENTITY_SHIM: "1" })).toBe(true);
|
||||
expect(identityShimEnabled({ ADAPTER_ENV: "staging", ADAPTER_APPLY_IDENTITY_SHIM: "0" })).toBe(false);
|
||||
expect(identityShimEnabled({ NODE_ENV: "development", ADAPTER_APPLY_IDENTITY_SHIM: "0" })).toBe(false);
|
||||
});
|
||||
|
||||
it("reconhece qual migration é o shim", () => {
|
||||
expect(isShimMigration("0001_platform_shim.sql")).toBe(true);
|
||||
expect(isShimMigration("0002_spreadsheet_adapter.sql")).toBe(false);
|
||||
expect(isShimMigration("0003_rls.sql")).toBe(false);
|
||||
});
|
||||
|
||||
it("inline é recusado em ambiente implantado mesmo se a variável pedir", () => {
|
||||
expect(jobRunsInline({ ADAPTER_ENV: "staging", ADAPTER_JOB_MODE: "inline" })).toBe(false);
|
||||
expect(jobRunsInline({ ADAPTER_ENV: "production", ADAPTER_JOB_MODE: "inline" })).toBe(false);
|
||||
expect(jobRunsInline({ NODE_ENV: "development", ADAPTER_JOB_MODE: "inline" })).toBe(true);
|
||||
expect(jobRunsInline({ NODE_ENV: "development", ADAPTER_JOB_MODE: "worker" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolução de workspace no servidor", () => {
|
||||
it("sem associação não há workspace", () => {
|
||||
expect(resolveWorkspace([], ALPHA)).toEqual({ kind: "none" });
|
||||
});
|
||||
|
||||
it("ignora a dica que não corresponde a associação real", () => {
|
||||
const result = resolveWorkspace([membership(BETA, "Beta")], ALPHA);
|
||||
expect(result).toEqual({ kind: "resolved", workspaceId: BETA });
|
||||
});
|
||||
|
||||
it("com várias associações e dica inválida, quem escolhe é a pessoa", () => {
|
||||
const result = resolveWorkspace([membership(ALPHA, "Alpha"), membership(BETA, "Beta")], ALPHA.replace(/1/g, "9"));
|
||||
expect(result.kind).toBe("choice");
|
||||
});
|
||||
|
||||
it("respeita a dica quando ela é uma associação real", () => {
|
||||
const result = resolveWorkspace([membership(ALPHA, "Alpha"), membership(BETA, "Beta")], BETA);
|
||||
expect(result).toEqual({ kind: "resolved", workspaceId: BETA });
|
||||
});
|
||||
|
||||
it("não aceita valor fora de UUID como dica", () => {
|
||||
const result = resolveWorkspace([membership(ALPHA, "Alpha"), membership(BETA, "Beta")], "' or 1=1 --");
|
||||
expect(result.kind).toBe("choice");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user