Skip to content

Commit 21f5827

Browse files
committed
RBAC tests: prompts (TRI-8738)
Prompts route family — read + update actions, both single-id ({type:"prompts", id: params.slug}) and collection-level ({type:"prompts", id: "all"}) resource shapes. Auth resolves before any DB lookup, so tests use non-existent slugs throughout; handler 404s but auth-passed assertion ("not 401, not 403") is what the matrix verifies. Coverage: Prompts list — GET /api/v1/prompts (5 cases): - missing auth → 401 - private API key → auth passes - JWT read:prompts → passes - JWT read:runs → 403 (type mismatch) - JWT admin → passes Prompts retrieve — GET /api/v1/prompts/:slug (7 cases, full matrix): - missing auth → 401 - private API key → passes - JWT read:prompts → passes - JWT read:prompts:<exact slug> → passes - JWT read:prompts:<other> → 403 - JWT read:runs → 403 (type mismatch) - JWT admin → passes Prompts override — POST /api/v1/prompts/:slug/override (6 cases): Tests the ACTION_ALIASES write→update behaviour: - missing auth → 401 - JWT write:prompts:<slug> matching → passes - JWT write:prompts (type-level) → passes - JWT read:prompts → 403 (action mismatch — read NOT aliased) - JWT write:prompts:<other> → 403 - JWT admin → passes Promote/reactivate sanity (2 cases): - promote: JWT write:prompts → passes - reactivate: JWT read:prompts → 403 Multi-method override (POST/PUT/PATCH/DELETE) is not exhaustively tested per-method — they share the same authorization config so covering POST suffices. If a method ever overrides authorization, add a targeted test. Verification: typecheck clean. Test execution still blocked by the e2e.full webapp-boot issue noted on TRI-8731.
1 parent e9e57f9 commit 21f5827

1 file changed

Lines changed: 291 additions & 0 deletions

File tree

apps/webapp/test/auth-api.e2e.full.test.ts

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1729,6 +1729,297 @@ describe("API", () => {
17291729
});
17301730
});
17311731

1732+
// Prompts routes (TRI-8738). Resource shapes:
1733+
// - List resource: { type: "prompts", id: "all" } action: read
1734+
// - Retrieve resource: { type: "prompts", id: params.slug } action: read
1735+
// - Override resource: { type: "prompts", id: params.slug } action: update
1736+
// (multi-method: POST/PUT/PATCH/DELETE)
1737+
// - Promote resource: { type: "prompts", id: params.slug } action: update
1738+
// - Reactivate resource: { type: "prompts", id: params.slug } action: update
1739+
//
1740+
// ACTION_ALIASES: update ← write, so write:prompts also satisfies
1741+
// the update-action routes.
1742+
//
1743+
// Auth happens before any DB lookup, so we test against
1744+
// non-existent slugs — handler will 404 but we assert "not 401/403"
1745+
// for pass cases.
1746+
describe("Prompts list — GET /api/v1/prompts (collection-level)", () => {
1747+
const path = "/api/v1/prompts";
1748+
const get = (headers: Record<string, string>) =>
1749+
getTestServer().webapp.fetch(path, { headers });
1750+
1751+
it("missing auth: 401", async () => {
1752+
const res = await getTestServer().webapp.fetch(path);
1753+
expect(res.status).toBe(401);
1754+
});
1755+
1756+
it("private API key: auth passes", async () => {
1757+
const server = getTestServer();
1758+
const seed = await seedTestEnvironment(server.prisma);
1759+
const res = await get({ Authorization: `Bearer ${seed.apiKey}` });
1760+
expect(res.status).not.toBe(401);
1761+
expect(res.status).not.toBe(403);
1762+
});
1763+
1764+
it("JWT read:prompts (type-level): auth passes", async () => {
1765+
const server = getTestServer();
1766+
const seed = await seedTestEnvironment(server.prisma);
1767+
const jwt = await generateJWT({
1768+
secretKey: seed.apiKey,
1769+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:prompts"] },
1770+
expirationTime: "15m",
1771+
});
1772+
const res = await get({ Authorization: `Bearer ${jwt}` });
1773+
expect(res.status).not.toBe(401);
1774+
expect(res.status).not.toBe(403);
1775+
});
1776+
1777+
it("JWT read:runs: 403 (type mismatch)", async () => {
1778+
const server = getTestServer();
1779+
const seed = await seedTestEnvironment(server.prisma);
1780+
const jwt = await generateJWT({
1781+
secretKey: seed.apiKey,
1782+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:runs"] },
1783+
expirationTime: "15m",
1784+
});
1785+
const res = await get({ Authorization: `Bearer ${jwt}` });
1786+
expect(res.status).toBe(403);
1787+
});
1788+
1789+
it("JWT admin: auth passes", async () => {
1790+
const server = getTestServer();
1791+
const seed = await seedTestEnvironment(server.prisma);
1792+
const jwt = await generateJWT({
1793+
secretKey: seed.apiKey,
1794+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
1795+
expirationTime: "15m",
1796+
});
1797+
const res = await get({ Authorization: `Bearer ${jwt}` });
1798+
expect(res.status).not.toBe(401);
1799+
expect(res.status).not.toBe(403);
1800+
});
1801+
});
1802+
1803+
describe("Prompts retrieve — GET /api/v1/prompts/:slug (id-keyed read)", () => {
1804+
const SLUG = "test-prompt";
1805+
const path = `/api/v1/prompts/${SLUG}`;
1806+
const get = (headers: Record<string, string>) =>
1807+
getTestServer().webapp.fetch(path, { headers });
1808+
1809+
it("missing auth: 401", async () => {
1810+
const res = await getTestServer().webapp.fetch(path);
1811+
expect(res.status).toBe(401);
1812+
});
1813+
1814+
it("private API key: auth passes", async () => {
1815+
const server = getTestServer();
1816+
const seed = await seedTestEnvironment(server.prisma);
1817+
const res = await get({ Authorization: `Bearer ${seed.apiKey}` });
1818+
expect(res.status).not.toBe(401);
1819+
expect(res.status).not.toBe(403);
1820+
});
1821+
1822+
it("JWT read:prompts (type-level): auth passes", async () => {
1823+
const server = getTestServer();
1824+
const seed = await seedTestEnvironment(server.prisma);
1825+
const jwt = await generateJWT({
1826+
secretKey: seed.apiKey,
1827+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:prompts"] },
1828+
expirationTime: "15m",
1829+
});
1830+
const res = await get({ Authorization: `Bearer ${jwt}` });
1831+
expect(res.status).not.toBe(401);
1832+
expect(res.status).not.toBe(403);
1833+
});
1834+
1835+
it("JWT read:prompts:<exact slug>: auth passes", async () => {
1836+
const server = getTestServer();
1837+
const seed = await seedTestEnvironment(server.prisma);
1838+
const jwt = await generateJWT({
1839+
secretKey: seed.apiKey,
1840+
payload: {
1841+
pub: true,
1842+
sub: seed.environment.id,
1843+
scopes: [`read:prompts:${SLUG}`],
1844+
},
1845+
expirationTime: "15m",
1846+
});
1847+
const res = await get({ Authorization: `Bearer ${jwt}` });
1848+
expect(res.status).not.toBe(401);
1849+
expect(res.status).not.toBe(403);
1850+
});
1851+
1852+
it("JWT read:prompts:<other>: 403", async () => {
1853+
const server = getTestServer();
1854+
const seed = await seedTestEnvironment(server.prisma);
1855+
const jwt = await generateJWT({
1856+
secretKey: seed.apiKey,
1857+
payload: {
1858+
pub: true,
1859+
sub: seed.environment.id,
1860+
scopes: ["read:prompts:some-other-slug"],
1861+
},
1862+
expirationTime: "15m",
1863+
});
1864+
const res = await get({ Authorization: `Bearer ${jwt}` });
1865+
expect(res.status).toBe(403);
1866+
});
1867+
1868+
it("JWT read:runs: 403 (type mismatch)", async () => {
1869+
const server = getTestServer();
1870+
const seed = await seedTestEnvironment(server.prisma);
1871+
const jwt = await generateJWT({
1872+
secretKey: seed.apiKey,
1873+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:runs"] },
1874+
expirationTime: "15m",
1875+
});
1876+
const res = await get({ Authorization: `Bearer ${jwt}` });
1877+
expect(res.status).toBe(403);
1878+
});
1879+
1880+
it("JWT admin: auth passes", async () => {
1881+
const server = getTestServer();
1882+
const seed = await seedTestEnvironment(server.prisma);
1883+
const jwt = await generateJWT({
1884+
secretKey: seed.apiKey,
1885+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
1886+
expirationTime: "15m",
1887+
});
1888+
const res = await get({ Authorization: `Bearer ${jwt}` });
1889+
expect(res.status).not.toBe(401);
1890+
expect(res.status).not.toBe(403);
1891+
});
1892+
});
1893+
1894+
describe("Prompts override — POST /api/v1/prompts/:slug/override (update action)", () => {
1895+
const SLUG = "test-prompt";
1896+
const path = `/api/v1/prompts/${SLUG}/override`;
1897+
const post = (headers: Record<string, string>) =>
1898+
getTestServer().webapp.fetch(path, {
1899+
method: "POST",
1900+
headers: { "Content-Type": "application/json", ...headers },
1901+
body: JSON.stringify({ content: "test" }),
1902+
});
1903+
1904+
it("missing auth: 401", async () => {
1905+
const res = await post({});
1906+
expect(res.status).toBe(401);
1907+
});
1908+
1909+
it("JWT write:prompts:<slug> matching (ACTION_ALIASES write→update): passes", async () => {
1910+
const server = getTestServer();
1911+
const seed = await seedTestEnvironment(server.prisma);
1912+
const jwt = await generateJWT({
1913+
secretKey: seed.apiKey,
1914+
payload: {
1915+
pub: true,
1916+
sub: seed.environment.id,
1917+
scopes: [`write:prompts:${SLUG}`],
1918+
},
1919+
expirationTime: "15m",
1920+
});
1921+
const res = await post({ Authorization: `Bearer ${jwt}` });
1922+
expect(res.status).not.toBe(401);
1923+
expect(res.status).not.toBe(403);
1924+
});
1925+
1926+
it("JWT write:prompts (type-level): passes", async () => {
1927+
const server = getTestServer();
1928+
const seed = await seedTestEnvironment(server.prisma);
1929+
const jwt = await generateJWT({
1930+
secretKey: seed.apiKey,
1931+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:prompts"] },
1932+
expirationTime: "15m",
1933+
});
1934+
const res = await post({ Authorization: `Bearer ${jwt}` });
1935+
expect(res.status).not.toBe(401);
1936+
expect(res.status).not.toBe(403);
1937+
});
1938+
1939+
it("JWT read:prompts: 403 (action mismatch — read NOT aliased to update)", async () => {
1940+
const server = getTestServer();
1941+
const seed = await seedTestEnvironment(server.prisma);
1942+
const jwt = await generateJWT({
1943+
secretKey: seed.apiKey,
1944+
payload: {
1945+
pub: true,
1946+
sub: seed.environment.id,
1947+
scopes: [`read:prompts:${SLUG}`],
1948+
},
1949+
expirationTime: "15m",
1950+
});
1951+
const res = await post({ Authorization: `Bearer ${jwt}` });
1952+
expect(res.status).toBe(403);
1953+
});
1954+
1955+
it("JWT write:prompts:<other>: 403", async () => {
1956+
const server = getTestServer();
1957+
const seed = await seedTestEnvironment(server.prisma);
1958+
const jwt = await generateJWT({
1959+
secretKey: seed.apiKey,
1960+
payload: {
1961+
pub: true,
1962+
sub: seed.environment.id,
1963+
scopes: ["write:prompts:some-other-slug"],
1964+
},
1965+
expirationTime: "15m",
1966+
});
1967+
const res = await post({ Authorization: `Bearer ${jwt}` });
1968+
expect(res.status).toBe(403);
1969+
});
1970+
1971+
it("JWT admin: passes", async () => {
1972+
const server = getTestServer();
1973+
const seed = await seedTestEnvironment(server.prisma);
1974+
const jwt = await generateJWT({
1975+
secretKey: seed.apiKey,
1976+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
1977+
expirationTime: "15m",
1978+
});
1979+
const res = await post({ Authorization: `Bearer ${jwt}` });
1980+
expect(res.status).not.toBe(401);
1981+
expect(res.status).not.toBe(403);
1982+
});
1983+
});
1984+
1985+
describe("Prompts promote/reactivate (sanity, update action)", () => {
1986+
it("promote: JWT write:prompts (type-level): auth passes", async () => {
1987+
const server = getTestServer();
1988+
const seed = await seedTestEnvironment(server.prisma);
1989+
const jwt = await generateJWT({
1990+
secretKey: seed.apiKey,
1991+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:prompts"] },
1992+
expirationTime: "15m",
1993+
});
1994+
const res = await server.webapp.fetch("/api/v1/prompts/some-slug/promote", {
1995+
method: "POST",
1996+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
1997+
body: JSON.stringify({}),
1998+
});
1999+
expect(res.status).not.toBe(401);
2000+
expect(res.status).not.toBe(403);
2001+
});
2002+
2003+
it("reactivate: JWT read:prompts: 403 (action mismatch)", async () => {
2004+
const server = getTestServer();
2005+
const seed = await seedTestEnvironment(server.prisma);
2006+
const jwt = await generateJWT({
2007+
secretKey: seed.apiKey,
2008+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:prompts"] },
2009+
expirationTime: "15m",
2010+
});
2011+
const res = await server.webapp.fetch(
2012+
"/api/v1/prompts/some-slug/override/reactivate",
2013+
{
2014+
method: "POST",
2015+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jwt}` },
2016+
body: JSON.stringify({}),
2017+
}
2018+
);
2019+
expect(res.status).toBe(403);
2020+
});
2021+
});
2022+
17322023
describe("Batch retrieve — GET /realtime/v1/batches/:batchId (sanity)", () => {
17332024
it("missing auth: 401", async () => {
17342025
const res = await getTestServer().webapp.fetch("/realtime/v1/batches/batch_anything");

0 commit comments

Comments
 (0)