Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,6 @@ jobs:
"ref": "${{ github.ref }}",
"lang": "bun",
"bun-version": "latest",
"test_cmd": "S2_LITE=1 bun run vitest --run --exclude '**/account-basin*' --exclude '**/accessTokens*' --exclude '**/metrics*' --exclude '**/spec.e2e*'"
"test_cmd": "S2_LITE=1 bun run vitest --run --exclude '**/account-basin*' --exclude '**/accessTokens*' --exclude '**/metrics*'"
}
]
48 changes: 48 additions & 0 deletions packages/streamstore/src/tests/accessTokens.spec.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,54 @@ describeIf("Access tokens spec parity", () => {
TEST_TIMEOUT_MS,
);

it(
"auto-prefixes streams when enabled",
async () => {
if (!setupOk) return;
if (!endpoints) return;

const tokenId = makeTokenId("ts-autoprefix-enforce");
const rawName = makeStreamName("apx");
const prefixedName = `tenant/${rawName}`;

const token = await issueToken({
id: tokenId,
autoPrefixStreams: true,
scope: {
basins: { exact: basinA },
streams: { prefix: "tenant/" },
ops: ["create-stream", "list-streams"],
},
});

const limited = new S2({
accessToken: token.accessToken,
endpoints,
});
const limitedBasin = limited.basin(basinA);
const adminBasin = s2.basin(basinA);

try {
await limitedBasin.streams.create({ stream: rawName });

const limitedList = await limitedBasin.streams.list({
prefix: rawName,
});
const limitedNames = limitedList.streams.map((s) => s.name);
expect(limitedNames).toContain(rawName);

const adminList = await adminBasin.streams.list({ prefix: "tenant/" });
const adminNames = adminList.streams.map((s) => s.name);
expect(adminNames).toContain(prefixedName);
} finally {
await adminBasin.streams
.delete({ stream: prefixedName })
.catch(() => {});
}
},
TEST_TIMEOUT_MS,
);

it(
"issues token with expiresAt",
async () => {
Expand Down
188 changes: 0 additions & 188 deletions packages/streamstore/src/tests/spec.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ type MeteredInput = Parameters<typeof meteredBytes>[0];

describeIf("Spec Integration Tests", () => {
let s2: S2;
let endpoints: S2ClientOptions["endpoints"];
let basinName: string;
let autoBasinName: string;
let basin: S2Basin;
let autoBasin: S2Basin;
let metricsStreamName: string;

const createStream = async (target: S2Basin, prefix: string) => {
const streamName = makeStreamName(prefix);
Expand All @@ -28,7 +26,6 @@ describeIf("Spec Integration Tests", () => {
beforeAll(async () => {
const env = S2Environment.parse();
if (!env.accessToken) return;
endpoints = env.endpoints;
s2 = new S2(env as S2ClientOptions);

basinName = makeBasinName("typescript-spec");
Expand All @@ -48,7 +45,6 @@ describeIf("Spec Integration Tests", () => {

basin = s2.basin(basinName);
autoBasin = s2.basin(autoBasinName);
metricsStreamName = await createStream(basin, "metrics");
}, TEST_TIMEOUT_MS);

afterAll(async () => {
Expand Down Expand Up @@ -330,188 +326,4 @@ describeIf("Spec Integration Tests", () => {
);
});

describe("Access token auto-prefix", () => {
it(
"rejects autoPrefixStreams with exact stream scope",
async () => {
const tokenId = `spec-autoprefix-exact-${Math.random()
.toString(36)
.slice(2, 10)}`;

await expect(
s2.accessTokens.issue({
id: tokenId,
autoPrefixStreams: true,
scope: {
basins: { prefix: "" },
streams: { exact: "tenant/stream" },
ops: ["create-stream"],
},
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);

it(
"creates and lists streams with auto-prefixing enabled",
async () => {
const tokenId = `spec-autoprefix-${Math.random()
.toString(36)
.slice(2, 10)}`;
const rawName = makeStreamName("apx");
const prefixedName = `tenant/${rawName}`;

const token = await s2.accessTokens.issue({
id: tokenId,
autoPrefixStreams: true,
scope: {
basins: { exact: basinName },
streams: { prefix: "tenant/" },
ops: ["create-stream", "list-streams"],
},
});

if (!endpoints) return;
const limited = new S2({
accessToken: token.accessToken,
endpoints,
});
const limitedBasin = limited.basin(basinName);

try {
await limitedBasin.streams.create({ stream: rawName });

const limitedList = await limitedBasin.streams.list({
prefix: rawName,
});
const limitedNames = limitedList.streams.map((s) => s.name);
expect(limitedNames).toContain(rawName);

const adminList = await basin.streams.list({ prefix: "tenant/" });
const adminNames = adminList.streams.map((s) => s.name);
expect(adminNames).toContain(prefixedName);
} finally {
await basin.streams.delete({ stream: prefixedName }).catch(() => {});
await s2.accessTokens.revoke({ id: tokenId }).catch(() => {});
}
},
TEST_TIMEOUT_MS,
);
});

describe("Metrics validation", () => {
const invalidRanges = [
{
name: "start-after-end",
build: () => {
const now = Date.now();
return { start: now, end: now - 3600 * 1000 };
},
},
{
name: "end-too-far-future",
build: () => {
const now = Date.now();
return { start: now - 3600 * 1000, end: now + 600 * 1000 };
},
},
{
name: "range-too-large",
build: () => {
const now = Date.now();
return { start: now - 40 * 24 * 3600 * 1000, end: now };
},
},
];

for (const tc of invalidRanges) {
it(
`rejects invalid account metric range (${tc.name})`,
async () => {
const { start, end } = tc.build();
await expect(
s2.metrics.account({
set: "active-basins",
start,
end,
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);

it(
`rejects invalid basin metric range (${tc.name})`,
async () => {
const { start, end } = tc.build();
await expect(
s2.metrics.basin({
basin: basinName,
set: "storage",
start,
end,
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);

it(
`rejects invalid stream metric range (${tc.name})`,
async () => {
const { start, end } = tc.build();
await expect(
s2.metrics.stream({
basin: basinName,
stream: metricsStreamName,
set: "storage",
start,
end,
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);
}

it(
"rejects basin storage interval other than hour",
async () => {
const now = Date.now();
const start = now - 3600 * 1000;
const end = now;
await expect(
s2.metrics.basin({
basin: basinName,
set: "storage",
start,
end,
interval: "minute",
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);

it(
"rejects stream storage interval other than minute",
async () => {
const now = Date.now();
const start = now - 3600 * 1000;
const end = now;
await expect(
s2.metrics.stream({
basin: basinName,
stream: metricsStreamName,
set: "storage",
start,
end,
interval: "hour",
}),
).rejects.toMatchObject({ status: 422 });
},
TEST_TIMEOUT_MS,
);
});
});
Loading