273 lines
8.2 KiB
TypeScript
273 lines
8.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
serviceStatusEnum,
|
|
createServiceSchema,
|
|
updateServiceSchema,
|
|
listServicesResponseSchema,
|
|
} from '@insforge/shared-schemas';
|
|
|
|
describe('serviceStatusEnum', () => {
|
|
it('accepts valid statuses', () => {
|
|
expect(serviceStatusEnum.safeParse('running').success).toBe(true);
|
|
expect(serviceStatusEnum.safeParse('stopped').success).toBe(true);
|
|
expect(serviceStatusEnum.safeParse('creating').success).toBe(true);
|
|
expect(serviceStatusEnum.safeParse('deploying').success).toBe(true);
|
|
expect(serviceStatusEnum.safeParse('failed').success).toBe(true);
|
|
expect(serviceStatusEnum.safeParse('destroying').success).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid status', () => {
|
|
expect(serviceStatusEnum.safeParse('banana').success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('createServiceSchema', () => {
|
|
it('validates a minimal valid request', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.cpu).toBe('shared-1x');
|
|
expect(result.data.memory).toBe(512);
|
|
expect(result.data.region).toBe('iad');
|
|
}
|
|
});
|
|
|
|
it('rejects name with uppercase', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'MyApi',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects name starting with dash', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: '-my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects name ending with dash', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api-',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('accepts missing imageUrl (source-mode prepareForDeploy sends no imageUrl; createService route guards image-mode)', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid port', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 0,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects invalid cpu tier', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
cpu: 'mega-cpu',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
// INS-271: --protocol tcp flag for raw TCP services (Redis,
|
|
// Postgres-wire-protocol). Without the schema accepting this field, the OSS
|
|
// would silently strip it before forwarding to the cloud-backend.
|
|
it('accepts protocol: tcp', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-redis',
|
|
imageUrl: 'redis:7',
|
|
port: 6379,
|
|
protocol: 'tcp',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.protocol).toBe('tcp');
|
|
}
|
|
});
|
|
|
|
it('accepts protocol: http (explicit)', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
protocol: 'http',
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.protocol).toBe('http');
|
|
}
|
|
});
|
|
|
|
it('omitting protocol is valid (back-compat default applied downstream)', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
// The schema itself leaves it undefined; services.service.ts is what
|
|
// falls back to 'http' at INSERT/Fly-call time.
|
|
expect(result.data.protocol).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('rejects unknown protocol values (udp, sctp, etc.)', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-svc',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
protocol: 'udp',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
// --always-on CLI flag: scaleToZero: false keeps the machine running 24/7
|
|
// instead of Fly stopping it when idle.
|
|
it('accepts scaleToZero: false', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
scaleToZero: false,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.scaleToZero).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('omitting scaleToZero is valid (back-compat default applied downstream)', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-api',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
// The schema itself leaves it undefined; services.service.ts is what
|
|
// falls back to true at INSERT/Fly-call time.
|
|
expect(result.data.scaleToZero).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it('rejects non-boolean scaleToZero', () => {
|
|
const result = createServiceSchema.safeParse({
|
|
name: 'my-svc',
|
|
imageUrl: 'node:20',
|
|
port: 8080,
|
|
scaleToZero: 'no',
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('listServicesResponseSchema', () => {
|
|
it('parses an empty services list', () => {
|
|
const result = listServicesResponseSchema.safeParse({ services: [] });
|
|
expect(result.success).toBe(true);
|
|
if (result.success) {
|
|
expect(result.data.services).toEqual([]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('updateServiceSchema', () => {
|
|
it('accepts partial update', () => {
|
|
const result = updateServiceSchema.safeParse({ imageUrl: 'node:21' });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts empty object', () => {
|
|
const result = updateServiceSchema.safeParse({});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts envVarsPatch with set only', () => {
|
|
const result = updateServiceSchema.safeParse({
|
|
envVarsPatch: { set: { DATABASE_URL: 'postgres://...' } },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('accepts envVarsPatch with unset only', () => {
|
|
const result = updateServiceSchema.safeParse({
|
|
envVarsPatch: { unset: ['STALE_SECRET'] },
|
|
});
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects empty envVarsPatch (no set, no unset) — would be a no-op API call', () => {
|
|
const result = updateServiceSchema.safeParse({ envVarsPatch: {} });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects envVarsPatch with invalid key in set', () => {
|
|
const result = updateServiceSchema.safeParse({
|
|
envVarsPatch: { set: { 'lower-case-key': 'v' } },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects envVarsPatch with invalid key in unset', () => {
|
|
const result = updateServiceSchema.safeParse({
|
|
envVarsPatch: { unset: ['lower'] },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it('rejects envVars and envVarsPatch sent together (ambiguous intent)', () => {
|
|
const result = updateServiceSchema.safeParse({
|
|
envVars: { ALL: 'replace' },
|
|
envVarsPatch: { set: { ONE: 'merge' } },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
// INS-271: protocol is updateable on existing services (rare — usually you'd
|
|
// delete + redeploy — but the schema must accept it so CLI's update path
|
|
// doesn't strip it).
|
|
it('accepts protocol: tcp on update', () => {
|
|
const result = updateServiceSchema.safeParse({ protocol: 'tcp' });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects invalid protocol on update', () => {
|
|
const result = updateServiceSchema.safeParse({ protocol: 'quic' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
// scaleToZero is updateable so an existing service can be flipped between
|
|
// scale-to-zero and always-on without a delete + redeploy.
|
|
it('accepts scaleToZero: false on update', () => {
|
|
const result = updateServiceSchema.safeParse({ scaleToZero: false });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
|
|
it('rejects non-boolean scaleToZero on update', () => {
|
|
const result = updateServiceSchema.safeParse({ scaleToZero: 'always' });
|
|
expect(result.success).toBe(false);
|
|
});
|
|
});
|