Skip to content

Commit 0fa5203

Browse files
Copilothotlong
andcommitted
test: add missing tests for TenantSchema extended fields, TenantUsageSchema, QuotaEnforcementResult, TenantRuntimeContext
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 8c5eee4 commit 0fa5203

2 files changed

Lines changed: 266 additions & 0 deletions

File tree

packages/spec/src/kernel/context.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
RuntimeMode,
44
KernelContextSchema,
55
PreviewModeConfigSchema,
6+
TenantRuntimeContextSchema,
67
type KernelContext,
8+
type TenantRuntimeContext,
79
} from './context.zod';
810

911
describe('RuntimeMode', () => {
@@ -167,3 +169,76 @@ describe('PreviewModeConfigSchema', () => {
167169
expect(() => PreviewModeConfigSchema.parse({ expiresInSeconds: 1.5 })).toThrow();
168170
});
169171
});
172+
173+
describe('TenantRuntimeContextSchema', () => {
174+
const baseContext = {
175+
instanceId: '550e8400-e29b-41d4-a716-446655440000',
176+
mode: 'production' as const,
177+
version: '1.0.0',
178+
cwd: '/app',
179+
startTime: Date.now(),
180+
features: {},
181+
};
182+
183+
it('should accept valid tenant runtime context', () => {
184+
const ctx: TenantRuntimeContext = {
185+
...baseContext,
186+
tenantId: 'tenant_abc',
187+
tenantPlan: 'pro',
188+
tenantRegion: 'us-east',
189+
tenantDbUrl: 'libsql://tenant-abc-myorg.turso.io',
190+
};
191+
const parsed = TenantRuntimeContextSchema.parse(ctx);
192+
expect(parsed.tenantId).toBe('tenant_abc');
193+
expect(parsed.tenantPlan).toBe('pro');
194+
expect(parsed.tenantRegion).toBe('us-east');
195+
expect(parsed.tenantDbUrl).toContain('turso.io');
196+
// Inherited from KernelContextSchema
197+
expect(parsed.instanceId).toBe('550e8400-e29b-41d4-a716-446655440000');
198+
expect(parsed.mode).toBe('production');
199+
});
200+
201+
it('should accept all tenant plans', () => {
202+
const plans = ['free', 'pro', 'enterprise'] as const;
203+
plans.forEach((plan) => {
204+
const parsed = TenantRuntimeContextSchema.parse({
205+
...baseContext,
206+
tenantId: 'tenant_test',
207+
tenantPlan: plan,
208+
tenantRegion: 'eu-west',
209+
tenantDbUrl: 'libsql://test.turso.io',
210+
});
211+
expect(parsed.tenantPlan).toBe(plan);
212+
});
213+
});
214+
215+
it('should reject missing tenant fields', () => {
216+
// Missing tenantId
217+
expect(() => TenantRuntimeContextSchema.parse({
218+
...baseContext,
219+
tenantPlan: 'free',
220+
tenantRegion: 'us-east',
221+
tenantDbUrl: 'libsql://test.turso.io',
222+
})).toThrow();
223+
});
224+
225+
it('should reject empty tenantId', () => {
226+
expect(() => TenantRuntimeContextSchema.parse({
227+
...baseContext,
228+
tenantId: '',
229+
tenantPlan: 'free',
230+
tenantRegion: 'us-east',
231+
tenantDbUrl: 'libsql://test.turso.io',
232+
})).toThrow();
233+
});
234+
235+
it('should reject invalid tenant plan', () => {
236+
expect(() => TenantRuntimeContextSchema.parse({
237+
...baseContext,
238+
tenantId: 'tenant_test',
239+
tenantPlan: 'basic',
240+
tenantRegion: 'us-east',
241+
tenantDbUrl: 'libsql://test.turso.io',
242+
})).toThrow();
243+
});
244+
});

packages/spec/src/system/tenant.test.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@ import {
88
DatabaseLevelIsolationStrategySchema,
99
TenantIsolationConfigSchema,
1010
TenantSecurityPolicySchema,
11+
DatabaseProviderSchema,
12+
TenantConnectionConfigSchema,
13+
TenantUsageSchema,
14+
QuotaEnforcementResultSchema,
1115
type Tenant,
1216
type TenantQuota,
1317
type RowLevelIsolationStrategy,
1418
type SchemaLevelIsolationStrategy,
1519
type DatabaseLevelIsolationStrategy,
1620
type TenantIsolationConfig,
1721
type TenantSecurityPolicy,
22+
type DatabaseProvider,
23+
type TenantConnectionConfig,
24+
type TenantUsage,
25+
type QuotaEnforcementResult,
1826
} from './tenant.zod';
1927

2028
describe('TenantIsolationLevel', () => {
@@ -505,3 +513,186 @@ describe('TenantSecurityPolicySchema', () => {
505513
expect(parsed.compliance?.auditRetentionDays).toBe(365);
506514
});
507515
});
516+
517+
describe('DatabaseProviderSchema', () => {
518+
it('should accept valid database providers', () => {
519+
const providers: DatabaseProvider[] = ['turso', 'postgres', 'memory'];
520+
providers.forEach((p) => {
521+
expect(() => DatabaseProviderSchema.parse(p)).not.toThrow();
522+
});
523+
});
524+
525+
it('should reject invalid database provider', () => {
526+
expect(() => DatabaseProviderSchema.parse('mysql')).toThrow();
527+
expect(() => DatabaseProviderSchema.parse('sqlite')).toThrow();
528+
});
529+
});
530+
531+
describe('TenantConnectionConfigSchema', () => {
532+
it('should accept full connection config', () => {
533+
const config: TenantConnectionConfig = {
534+
url: 'libsql://tenant-abc-myorg.turso.io',
535+
authToken: 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...',
536+
group: 'production',
537+
};
538+
const parsed = TenantConnectionConfigSchema.parse(config);
539+
expect(parsed.url).toBe('libsql://tenant-abc-myorg.turso.io');
540+
expect(parsed.authToken).toBeDefined();
541+
expect(parsed.group).toBe('production');
542+
});
543+
544+
it('should accept minimal connection config (url only)', () => {
545+
const config = { url: 'file:local.db' };
546+
const parsed = TenantConnectionConfigSchema.parse(config);
547+
expect(parsed.url).toBe('file:local.db');
548+
expect(parsed.authToken).toBeUndefined();
549+
expect(parsed.group).toBeUndefined();
550+
});
551+
552+
it('should reject missing url', () => {
553+
expect(() => TenantConnectionConfigSchema.parse({})).toThrow();
554+
});
555+
556+
it('should reject empty url', () => {
557+
expect(() => TenantConnectionConfigSchema.parse({ url: '' })).toThrow();
558+
});
559+
});
560+
561+
describe('TenantSchema (extended fields)', () => {
562+
it('should accept tenant with Turso provisioning fields', () => {
563+
const tenant = {
564+
id: 'tenant_turso_001',
565+
name: 'Turso Tenant',
566+
isolationLevel: 'isolated_db',
567+
databaseProvider: 'turso',
568+
connectionConfig: {
569+
url: 'libsql://tenant-001-myorg.turso.io',
570+
authToken: 'jwt-token',
571+
group: 'production',
572+
},
573+
provisioningStatus: 'active',
574+
plan: 'pro',
575+
};
576+
const parsed = TenantSchema.parse(tenant);
577+
expect(parsed.databaseProvider).toBe('turso');
578+
expect(parsed.connectionConfig?.url).toContain('turso.io');
579+
expect(parsed.provisioningStatus).toBe('active');
580+
expect(parsed.plan).toBe('pro');
581+
});
582+
583+
it('should accept all provisioning statuses', () => {
584+
const statuses = ['provisioning', 'active', 'suspended', 'failed', 'destroying'];
585+
statuses.forEach((status) => {
586+
const tenant = {
587+
id: 'tenant_status',
588+
name: 'Status Test',
589+
isolationLevel: 'isolated_db',
590+
provisioningStatus: status,
591+
};
592+
expect(() => TenantSchema.parse(tenant)).not.toThrow();
593+
});
594+
});
595+
596+
it('should accept all plan values', () => {
597+
const plans = ['free', 'pro', 'enterprise'];
598+
plans.forEach((plan) => {
599+
const tenant = {
600+
id: 'tenant_plan',
601+
name: 'Plan Test',
602+
isolationLevel: 'isolated_db',
603+
plan,
604+
};
605+
expect(() => TenantSchema.parse(tenant)).not.toThrow();
606+
});
607+
});
608+
609+
it('should remain backward-compatible (new fields are optional)', () => {
610+
const legacyTenant = {
611+
id: 'tenant_legacy',
612+
name: 'Legacy Tenant',
613+
isolationLevel: 'shared_schema',
614+
};
615+
const parsed = TenantSchema.parse(legacyTenant);
616+
expect(parsed.databaseProvider).toBeUndefined();
617+
expect(parsed.connectionConfig).toBeUndefined();
618+
expect(parsed.provisioningStatus).toBeUndefined();
619+
expect(parsed.plan).toBeUndefined();
620+
});
621+
});
622+
623+
describe('TenantQuotaSchema (extended fields)', () => {
624+
it('should accept extended quota fields', () => {
625+
const quota: TenantQuota = {
626+
maxUsers: 100,
627+
maxStorage: 10737418240,
628+
apiRateLimit: 1000,
629+
maxObjects: 50,
630+
maxRecordsPerObject: 100000,
631+
maxDeploymentsPerDay: 10,
632+
maxStorageBytes: 5368709120,
633+
};
634+
const parsed = TenantQuotaSchema.parse(quota);
635+
expect(parsed.maxObjects).toBe(50);
636+
expect(parsed.maxRecordsPerObject).toBe(100000);
637+
expect(parsed.maxDeploymentsPerDay).toBe(10);
638+
expect(parsed.maxStorageBytes).toBe(5368709120);
639+
});
640+
641+
it('should accept empty quota (all optional)', () => {
642+
const parsed = TenantQuotaSchema.parse({});
643+
expect(parsed.maxObjects).toBeUndefined();
644+
expect(parsed.maxRecordsPerObject).toBeUndefined();
645+
});
646+
});
647+
648+
describe('TenantUsageSchema', () => {
649+
it('should accept full usage tracking', () => {
650+
const usage: TenantUsage = {
651+
objectCount: 15,
652+
totalRecords: 50000,
653+
storageBytes: 1073741824,
654+
deploymentsToday: 3,
655+
lastUpdatedAt: '2026-01-15T10:30:00Z',
656+
};
657+
const parsed = TenantUsageSchema.parse(usage);
658+
expect(parsed.objectCount).toBe(15);
659+
expect(parsed.totalRecords).toBe(50000);
660+
expect(parsed.deploymentsToday).toBe(3);
661+
});
662+
663+
it('should apply zero defaults', () => {
664+
const parsed = TenantUsageSchema.parse({});
665+
expect(parsed.objectCount).toBe(0);
666+
expect(parsed.totalRecords).toBe(0);
667+
expect(parsed.storageBytes).toBe(0);
668+
expect(parsed.deploymentsToday).toBe(0);
669+
});
670+
671+
it('should reject negative values', () => {
672+
expect(() => TenantUsageSchema.parse({ objectCount: -1 })).toThrow();
673+
});
674+
});
675+
676+
describe('QuotaEnforcementResultSchema', () => {
677+
it('should accept allowed result', () => {
678+
const result: QuotaEnforcementResult = {
679+
allowed: true,
680+
};
681+
expect(() => QuotaEnforcementResultSchema.parse(result)).not.toThrow();
682+
});
683+
684+
it('should accept denied result with details', () => {
685+
const result: QuotaEnforcementResult = {
686+
allowed: false,
687+
exceededQuota: 'maxObjects',
688+
currentUsage: 50,
689+
limit: 50,
690+
message: 'Maximum number of custom objects reached (50/50)',
691+
};
692+
const parsed = QuotaEnforcementResultSchema.parse(result);
693+
expect(parsed.allowed).toBe(false);
694+
expect(parsed.exceededQuota).toBe('maxObjects');
695+
expect(parsed.currentUsage).toBe(50);
696+
expect(parsed.limit).toBe(50);
697+
});
698+
});

0 commit comments

Comments
 (0)