@auxiora/cloud 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -4
- package/src/billing.ts +0 -142
- package/src/index.ts +0 -21
- package/src/isolation.ts +0 -69
- package/src/plans.ts +0 -84
- package/src/quotas.ts +0 -103
- package/src/tenant.ts +0 -140
- package/src/types.ts +0 -114
- package/tests/billing.test.ts +0 -126
- package/tests/isolation.test.ts +0 -95
- package/tests/plans.test.ts +0 -70
- package/tests/quotas.test.ts +0 -115
- package/tests/tenant.test.ts +0 -149
- package/tsconfig.json +0 -13
- package/tsconfig.tsbuildinfo +0 -1
package/tests/billing.test.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import { TenantManager } from '../src/tenant.js';
|
|
6
|
-
import { BillingManager } from '../src/billing.js';
|
|
7
|
-
import type { CloudConfig, StripeClient } from '../src/types.js';
|
|
8
|
-
|
|
9
|
-
function makeConfig(baseDataDir: string): CloudConfig {
|
|
10
|
-
return {
|
|
11
|
-
enabled: true,
|
|
12
|
-
baseDataDir,
|
|
13
|
-
jwtSecret: 'test-secret-32-chars-long-enough!',
|
|
14
|
-
domain: 'test.auxiora.cloud',
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function createMockStripe(): StripeClient {
|
|
19
|
-
return {
|
|
20
|
-
createCustomer: vi.fn().mockResolvedValue({ id: 'cus_test123' }),
|
|
21
|
-
createSubscription: vi.fn().mockResolvedValue({ id: 'sub_test123', status: 'active' }),
|
|
22
|
-
cancelSubscription: vi.fn().mockResolvedValue(undefined),
|
|
23
|
-
getInvoices: vi.fn().mockResolvedValue([
|
|
24
|
-
{ id: 'inv_1', amount: 1900, status: 'paid', created: '2025-01-01T00:00:00Z' },
|
|
25
|
-
]),
|
|
26
|
-
createPaymentIntent: vi.fn().mockResolvedValue({ clientSecret: 'pi_secret' }),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe('BillingManager', () => {
|
|
31
|
-
let tenantManager: TenantManager;
|
|
32
|
-
let billing: BillingManager;
|
|
33
|
-
let mockStripe: StripeClient;
|
|
34
|
-
let tmpDir: string;
|
|
35
|
-
|
|
36
|
-
beforeEach(async () => {
|
|
37
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cloud-billing-'));
|
|
38
|
-
tenantManager = new TenantManager({ config: makeConfig(tmpDir) });
|
|
39
|
-
mockStripe = createMockStripe();
|
|
40
|
-
billing = new BillingManager({ tenantManager, stripe: mockStripe });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(async () => {
|
|
44
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe('createCustomer', () => {
|
|
48
|
-
it('should create a Stripe customer', async () => {
|
|
49
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
50
|
-
const customerId = await billing.createCustomer(tenant.id);
|
|
51
|
-
expect(customerId).toBe('cus_test123');
|
|
52
|
-
expect(mockStripe.createCustomer).toHaveBeenCalledWith('alice@test.com', 'Alice');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should throw if Stripe not configured', async () => {
|
|
56
|
-
const billingNoStripe = new BillingManager({ tenantManager });
|
|
57
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
58
|
-
await expect(billingNoStripe.createCustomer(tenant.id)).rejects.toThrow('Stripe not configured');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('createSubscription', () => {
|
|
63
|
-
it('should create a subscription', async () => {
|
|
64
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
65
|
-
// Manually set stripeCustomerId
|
|
66
|
-
(tenant as any).stripeCustomerId = 'cus_test123';
|
|
67
|
-
|
|
68
|
-
const result = await billing.createSubscription(tenant.id, 'pro');
|
|
69
|
-
expect(result.subscriptionId).toBe('sub_test123');
|
|
70
|
-
expect(result.status).toBe('active');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should throw without customer ID', async () => {
|
|
74
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
75
|
-
await expect(billing.createSubscription(tenant.id, 'pro')).rejects.toThrow('No Stripe customer ID');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('getBillingInfo', () => {
|
|
80
|
-
it('should return billing info', async () => {
|
|
81
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'pro');
|
|
82
|
-
const info = await billing.getBillingInfo(tenant.id);
|
|
83
|
-
expect(info.tenantId).toBe(tenant.id);
|
|
84
|
-
expect(info.plan).toBe('pro');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe('getInvoices', () => {
|
|
89
|
-
it('should return invoices from Stripe', async () => {
|
|
90
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
91
|
-
(tenant as any).stripeCustomerId = 'cus_test123';
|
|
92
|
-
|
|
93
|
-
const invoices = await billing.getInvoices(tenant.id);
|
|
94
|
-
expect(invoices).toHaveLength(1);
|
|
95
|
-
expect(invoices[0].id).toBe('inv_1');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should return empty for tenant without customer', async () => {
|
|
99
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
100
|
-
const invoices = await billing.getInvoices(tenant.id);
|
|
101
|
-
expect(invoices).toHaveLength(0);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('handleWebhookEvent', () => {
|
|
106
|
-
it('should downgrade on subscription deleted', async () => {
|
|
107
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'pro');
|
|
108
|
-
(tenant as any).stripeCustomerId = 'cus_alice';
|
|
109
|
-
|
|
110
|
-
await billing.handleWebhookEvent('customer.subscription.deleted', { customer: 'cus_alice' });
|
|
111
|
-
|
|
112
|
-
const updated = await tenantManager.get(tenant.id);
|
|
113
|
-
expect(updated.plan).toBe('free');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should suspend on payment failed', async () => {
|
|
117
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'pro');
|
|
118
|
-
(tenant as any).stripeCustomerId = 'cus_alice';
|
|
119
|
-
|
|
120
|
-
await billing.handleWebhookEvent('invoice.payment_failed', { customer: 'cus_alice' });
|
|
121
|
-
|
|
122
|
-
const updated = await tenantManager.get(tenant.id);
|
|
123
|
-
expect(updated.status).toBe('suspended');
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
});
|
package/tests/isolation.test.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import { TenantManager } from '../src/tenant.js';
|
|
6
|
-
import { TenantIsolation } from '../src/isolation.js';
|
|
7
|
-
import { TenantNotFoundError, TenantSuspendedError } from '../src/types.js';
|
|
8
|
-
import type { CloudConfig } from '../src/types.js';
|
|
9
|
-
|
|
10
|
-
function makeConfig(baseDataDir: string): CloudConfig {
|
|
11
|
-
return {
|
|
12
|
-
enabled: true,
|
|
13
|
-
baseDataDir,
|
|
14
|
-
jwtSecret: 'test-secret-32-chars-long-enough!',
|
|
15
|
-
domain: 'test.auxiora.cloud',
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('TenantIsolation', () => {
|
|
20
|
-
let tenantManager: TenantManager;
|
|
21
|
-
let isolation: TenantIsolation;
|
|
22
|
-
let tmpDir: string;
|
|
23
|
-
let config: CloudConfig;
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cloud-isolation-'));
|
|
27
|
-
config = makeConfig(tmpDir);
|
|
28
|
-
tenantManager = new TenantManager({ config });
|
|
29
|
-
isolation = new TenantIsolation({ tenantManager, config });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(async () => {
|
|
33
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('extractTenant', () => {
|
|
37
|
-
it('should extract tenant from JWT payload with tenantId', async () => {
|
|
38
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
39
|
-
const context = await isolation.extractTenant({ tenantId: tenant.id });
|
|
40
|
-
expect(context.tenantId).toBe(tenant.id);
|
|
41
|
-
expect(context.tenant.email).toBe('alice@test.com');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should extract tenant from JWT payload with sub', async () => {
|
|
45
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
46
|
-
const context = await isolation.extractTenant({ sub: tenant.id });
|
|
47
|
-
expect(context.tenantId).toBe(tenant.id);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should throw for missing tenant ID', async () => {
|
|
51
|
-
await expect(isolation.extractTenant({})).rejects.toThrow(TenantNotFoundError);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should throw for suspended tenant', async () => {
|
|
55
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
56
|
-
await tenantManager.suspend(tenant.id, 'overdue');
|
|
57
|
-
await expect(isolation.extractTenant({ tenantId: tenant.id })).rejects.toThrow(TenantSuspendedError);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should throw for deleted tenant', async () => {
|
|
61
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
62
|
-
await tenantManager.delete(tenant.id);
|
|
63
|
-
await expect(isolation.extractTenant({ tenantId: tenant.id })).rejects.toThrow(TenantNotFoundError);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('scopePath', () => {
|
|
68
|
-
it('should resolve paths within tenant dir', async () => {
|
|
69
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
70
|
-
const scoped = isolation.scopePath(tenant, 'config/settings.json');
|
|
71
|
-
expect(scoped).toContain(tenant.id);
|
|
72
|
-
expect(scoped).toContain('config/settings.json');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should block path traversal', async () => {
|
|
76
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
77
|
-
expect(() => isolation.scopePath(tenant, '../../etc/passwd')).toThrow('Path traversal');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('validateAccess', () => {
|
|
82
|
-
it('should allow access to own resources', async () => {
|
|
83
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
84
|
-
const context = await isolation.extractTenant({ tenantId: tenant.id });
|
|
85
|
-
expect(isolation.validateAccess(context, tenant.id)).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should deny access to other tenant resources', async () => {
|
|
89
|
-
const alice = await tenantManager.create('alice@test.com', 'Alice');
|
|
90
|
-
const bob = await tenantManager.create('bob@test.com', 'Bob');
|
|
91
|
-
const context = await isolation.extractTenant({ tenantId: alice.id });
|
|
92
|
-
expect(isolation.validateAccess(context, bob.id)).toBe(false);
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
});
|
package/tests/plans.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { getPlanDefinition, getPlanQuotas, getAllPlans, isValidPlan } from '../src/plans.js';
|
|
3
|
-
|
|
4
|
-
describe('Plans', () => {
|
|
5
|
-
describe('getPlanDefinition', () => {
|
|
6
|
-
it('should return free plan', () => {
|
|
7
|
-
const plan = getPlanDefinition('free');
|
|
8
|
-
expect(plan.name).toBe('Free');
|
|
9
|
-
expect(plan.priceMonthly).toBe(0);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should return pro plan', () => {
|
|
13
|
-
const plan = getPlanDefinition('pro');
|
|
14
|
-
expect(plan.name).toBe('Pro');
|
|
15
|
-
expect(plan.priceMonthly).toBe(19);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should return team plan', () => {
|
|
19
|
-
const plan = getPlanDefinition('team');
|
|
20
|
-
expect(plan.name).toBe('Team');
|
|
21
|
-
expect(plan.priceMonthly).toBe(49);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should return enterprise plan', () => {
|
|
25
|
-
const plan = getPlanDefinition('enterprise');
|
|
26
|
-
expect(plan.name).toBe('Enterprise');
|
|
27
|
-
expect(plan.quotas.maxMessages).toBe(-1);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('getPlanQuotas', () => {
|
|
32
|
-
it('should return quotas for free plan', () => {
|
|
33
|
-
const quotas = getPlanQuotas('free');
|
|
34
|
-
expect(quotas.maxMessages).toBe(100);
|
|
35
|
-
expect(quotas.maxSessions).toBe(3);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return unlimited for enterprise', () => {
|
|
39
|
-
const quotas = getPlanQuotas('enterprise');
|
|
40
|
-
expect(quotas.maxMessages).toBe(-1);
|
|
41
|
-
expect(quotas.maxSessions).toBe(-1);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('getAllPlans', () => {
|
|
46
|
-
it('should return all four plans', () => {
|
|
47
|
-
const plans = getAllPlans();
|
|
48
|
-
expect(plans).toHaveLength(4);
|
|
49
|
-
const names = plans.map(p => p.plan);
|
|
50
|
-
expect(names).toContain('free');
|
|
51
|
-
expect(names).toContain('pro');
|
|
52
|
-
expect(names).toContain('team');
|
|
53
|
-
expect(names).toContain('enterprise');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('isValidPlan', () => {
|
|
58
|
-
it('should return true for valid plans', () => {
|
|
59
|
-
expect(isValidPlan('free')).toBe(true);
|
|
60
|
-
expect(isValidPlan('pro')).toBe(true);
|
|
61
|
-
expect(isValidPlan('team')).toBe(true);
|
|
62
|
-
expect(isValidPlan('enterprise')).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should return false for invalid plans', () => {
|
|
66
|
-
expect(isValidPlan('platinum')).toBe(false);
|
|
67
|
-
expect(isValidPlan('')).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
});
|
package/tests/quotas.test.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import { TenantManager } from '../src/tenant.js';
|
|
6
|
-
import { QuotaEnforcer } from '../src/quotas.js';
|
|
7
|
-
import { QuotaExceededError } from '../src/types.js';
|
|
8
|
-
import type { CloudConfig } from '../src/types.js';
|
|
9
|
-
|
|
10
|
-
function makeConfig(baseDataDir: string): CloudConfig {
|
|
11
|
-
return {
|
|
12
|
-
enabled: true,
|
|
13
|
-
baseDataDir,
|
|
14
|
-
jwtSecret: 'test-secret-32-chars-long-enough!',
|
|
15
|
-
domain: 'test.auxiora.cloud',
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('QuotaEnforcer', () => {
|
|
20
|
-
let tenantManager: TenantManager;
|
|
21
|
-
let enforcer: QuotaEnforcer;
|
|
22
|
-
let tmpDir: string;
|
|
23
|
-
|
|
24
|
-
beforeEach(async () => {
|
|
25
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cloud-quota-'));
|
|
26
|
-
tenantManager = new TenantManager({ config: makeConfig(tmpDir) });
|
|
27
|
-
enforcer = new QuotaEnforcer({ tenantManager });
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
afterEach(async () => {
|
|
31
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
describe('check', () => {
|
|
35
|
-
it('should pass when under quota', async () => {
|
|
36
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'free');
|
|
37
|
-
await expect(enforcer.check(tenant.id, 'maxMessages')).resolves.toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should throw QuotaExceededError when over limit', async () => {
|
|
41
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'free');
|
|
42
|
-
// Free plan: 100 messages
|
|
43
|
-
for (let i = 0; i < 100; i++) {
|
|
44
|
-
enforcer.record(tenant.id, 'maxMessages');
|
|
45
|
-
}
|
|
46
|
-
await expect(enforcer.check(tenant.id, 'maxMessages')).rejects.toThrow(QuotaExceededError);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should not throw for enterprise (unlimited)', async () => {
|
|
50
|
-
const tenant = await tenantManager.create('corp@test.com', 'Corp', 'enterprise');
|
|
51
|
-
for (let i = 0; i < 10000; i++) {
|
|
52
|
-
enforcer.record(tenant.id, 'maxMessages');
|
|
53
|
-
}
|
|
54
|
-
await expect(enforcer.check(tenant.id, 'maxMessages')).resolves.toBeUndefined();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('record', () => {
|
|
59
|
-
it('should track usage', async () => {
|
|
60
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
61
|
-
enforcer.record(tenant.id, 'maxMessages');
|
|
62
|
-
enforcer.record(tenant.id, 'maxMessages');
|
|
63
|
-
expect(enforcer.getUsage(tenant.id, 'maxMessages')).toBe(2);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should return usage record', async () => {
|
|
67
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
68
|
-
const record = enforcer.record(tenant.id, 'maxSessions', 3);
|
|
69
|
-
expect(record.tenantId).toBe(tenant.id);
|
|
70
|
-
expect(record.metric).toBe('maxSessions');
|
|
71
|
-
expect(record.value).toBe(3);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('getAllUsage', () => {
|
|
76
|
-
it('should return all metrics', async () => {
|
|
77
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
78
|
-
enforcer.record(tenant.id, 'maxMessages', 10);
|
|
79
|
-
enforcer.record(tenant.id, 'maxSessions', 2);
|
|
80
|
-
const usage = enforcer.getAllUsage(tenant.id);
|
|
81
|
-
expect(usage.maxMessages).toBe(10);
|
|
82
|
-
expect(usage.maxSessions).toBe(2);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should return empty for unknown tenant', () => {
|
|
86
|
-
const usage = enforcer.getAllUsage('nonexistent');
|
|
87
|
-
expect(usage).toEqual({});
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('resetUsage', () => {
|
|
92
|
-
it('should reset all usage for a tenant', async () => {
|
|
93
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice');
|
|
94
|
-
enforcer.record(tenant.id, 'maxMessages', 50);
|
|
95
|
-
enforcer.resetUsage(tenant.id);
|
|
96
|
-
expect(enforcer.getUsage(tenant.id, 'maxMessages')).toBe(0);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('getUsagePercentage', () => {
|
|
101
|
-
it('should calculate percentage', async () => {
|
|
102
|
-
const tenant = await tenantManager.create('alice@test.com', 'Alice', 'free');
|
|
103
|
-
enforcer.record(tenant.id, 'maxMessages', 50);
|
|
104
|
-
const pct = await enforcer.getUsagePercentage(tenant.id, 'maxMessages');
|
|
105
|
-
expect(pct).toBe(50);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return 0 for enterprise (unlimited)', async () => {
|
|
109
|
-
const tenant = await tenantManager.create('corp@test.com', 'Corp', 'enterprise');
|
|
110
|
-
enforcer.record(tenant.id, 'maxMessages', 999);
|
|
111
|
-
const pct = await enforcer.getUsagePercentage(tenant.id, 'maxMessages');
|
|
112
|
-
expect(pct).toBe(0);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
});
|
package/tests/tenant.test.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs/promises';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import * as os from 'node:os';
|
|
5
|
-
import { TenantManager } from '../src/tenant.js';
|
|
6
|
-
import { TenantNotFoundError } from '../src/types.js';
|
|
7
|
-
import type { CloudConfig } from '../src/types.js';
|
|
8
|
-
|
|
9
|
-
function makeConfig(baseDataDir: string): CloudConfig {
|
|
10
|
-
return {
|
|
11
|
-
enabled: true,
|
|
12
|
-
baseDataDir,
|
|
13
|
-
jwtSecret: 'test-secret-32-chars-long-enough!',
|
|
14
|
-
domain: 'test.auxiora.cloud',
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('TenantManager', () => {
|
|
19
|
-
let manager: TenantManager;
|
|
20
|
-
let tmpDir: string;
|
|
21
|
-
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cloud-tenant-'));
|
|
24
|
-
manager = new TenantManager({ config: makeConfig(tmpDir) });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('create', () => {
|
|
32
|
-
it('should create a tenant with defaults', async () => {
|
|
33
|
-
const tenant = await manager.create('alice@test.com', 'Alice');
|
|
34
|
-
expect(tenant.id).toMatch(/^tenant-/);
|
|
35
|
-
expect(tenant.email).toBe('alice@test.com');
|
|
36
|
-
expect(tenant.name).toBe('Alice');
|
|
37
|
-
expect(tenant.plan).toBe('free');
|
|
38
|
-
expect(tenant.status).toBe('active');
|
|
39
|
-
expect(tenant.dataDir).toContain(tenant.id);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should create a tenant with a specific plan', async () => {
|
|
43
|
-
const tenant = await manager.create('bob@test.com', 'Bob', 'pro');
|
|
44
|
-
expect(tenant.plan).toBe('pro');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should persist tenant to disk', async () => {
|
|
48
|
-
const tenant = await manager.create('charlie@test.com', 'Charlie');
|
|
49
|
-
const tenantFile = path.join(tenant.dataDir, 'tenant.json');
|
|
50
|
-
const content = await fs.readFile(tenantFile, 'utf-8');
|
|
51
|
-
const persisted = JSON.parse(content);
|
|
52
|
-
expect(persisted.email).toBe('charlie@test.com');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('get', () => {
|
|
57
|
-
it('should return an existing tenant', async () => {
|
|
58
|
-
const created = await manager.create('alice@test.com', 'Alice');
|
|
59
|
-
const found = await manager.get(created.id);
|
|
60
|
-
expect(found.email).toBe('alice@test.com');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should throw TenantNotFoundError for unknown id', async () => {
|
|
64
|
-
await expect(manager.get('nonexistent')).rejects.toThrow(TenantNotFoundError);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('getByEmail', () => {
|
|
69
|
-
it('should find a tenant by email', async () => {
|
|
70
|
-
await manager.create('alice@test.com', 'Alice');
|
|
71
|
-
const found = await manager.getByEmail('alice@test.com');
|
|
72
|
-
expect(found).not.toBeNull();
|
|
73
|
-
expect(found!.name).toBe('Alice');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should return null for unknown email', async () => {
|
|
77
|
-
const found = await manager.getByEmail('nobody@test.com');
|
|
78
|
-
expect(found).toBeNull();
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('update', () => {
|
|
83
|
-
it('should update tenant fields', async () => {
|
|
84
|
-
const tenant = await manager.create('alice@test.com', 'Alice');
|
|
85
|
-
const updated = await manager.update(tenant.id, { name: 'Alice Smith', plan: 'pro' });
|
|
86
|
-
expect(updated.name).toBe('Alice Smith');
|
|
87
|
-
expect(updated.plan).toBe('pro');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('suspend / reactivate', () => {
|
|
92
|
-
it('should suspend and reactivate a tenant', async () => {
|
|
93
|
-
const tenant = await manager.create('alice@test.com', 'Alice');
|
|
94
|
-
|
|
95
|
-
const suspended = await manager.suspend(tenant.id, 'Payment failed');
|
|
96
|
-
expect(suspended.status).toBe('suspended');
|
|
97
|
-
expect(suspended.suspendReason).toBe('Payment failed');
|
|
98
|
-
|
|
99
|
-
const reactivated = await manager.reactivate(tenant.id);
|
|
100
|
-
expect(reactivated.status).toBe('active');
|
|
101
|
-
expect(reactivated.suspendedAt).toBeUndefined();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
describe('delete', () => {
|
|
106
|
-
it('should mark tenant as deleted', async () => {
|
|
107
|
-
const tenant = await manager.create('alice@test.com', 'Alice');
|
|
108
|
-
const result = await manager.delete(tenant.id);
|
|
109
|
-
expect(result).toBe(true);
|
|
110
|
-
|
|
111
|
-
const list = await manager.list();
|
|
112
|
-
expect(list).toHaveLength(0);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should return false for unknown tenant', async () => {
|
|
116
|
-
const result = await manager.delete('nonexistent');
|
|
117
|
-
expect(result).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('list', () => {
|
|
122
|
-
it('should list active tenants', async () => {
|
|
123
|
-
await manager.create('alice@test.com', 'Alice');
|
|
124
|
-
await manager.create('bob@test.com', 'Bob');
|
|
125
|
-
const list = await manager.list();
|
|
126
|
-
expect(list).toHaveLength(2);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('should exclude deleted tenants', async () => {
|
|
130
|
-
const a = await manager.create('alice@test.com', 'Alice');
|
|
131
|
-
await manager.create('bob@test.com', 'Bob');
|
|
132
|
-
await manager.delete(a.id);
|
|
133
|
-
const list = await manager.list();
|
|
134
|
-
expect(list).toHaveLength(1);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('loadFromDisk', () => {
|
|
139
|
-
it('should reload tenants from disk', async () => {
|
|
140
|
-
const tenant = await manager.create('alice@test.com', 'Alice');
|
|
141
|
-
|
|
142
|
-
// Create new manager and load
|
|
143
|
-
const manager2 = new TenantManager({ config: makeConfig(tmpDir) });
|
|
144
|
-
await manager2.loadFromDisk();
|
|
145
|
-
const found = await manager2.get(tenant.id);
|
|
146
|
-
expect(found.email).toBe('alice@test.com');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
});
|
package/tsconfig.json
DELETED