d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
249 lines
9.2 KiB
TypeScript
249 lines
9.2 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { ForkRemapKind } from '@/ee/workspace-forking/lib/remap/remap-references'
|
|
|
|
const { mockFilterExisting, mockGetCredentialProviders, mockGetEnvKeys } = vi.hoisted(() => ({
|
|
mockFilterExisting: vi.fn(),
|
|
mockGetCredentialProviders: vi.fn(),
|
|
mockGetEnvKeys: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/ee/workspace-forking/lib/mapping/resources', () => ({
|
|
listForkResourceCandidates: vi.fn(),
|
|
classifyCredentialResourceType: vi.fn(),
|
|
getWorkspaceEnvKeys: mockGetEnvKeys,
|
|
filterExistingForkTargets: mockFilterExisting,
|
|
getCredentialProvidersByIds: mockGetCredentialProviders,
|
|
CANDIDATE_LIMIT: 1000,
|
|
}))
|
|
|
|
import { ForkError } from '@/ee/workspace-forking/lib/lineage/authz'
|
|
import {
|
|
findDuplicateTargetEntry,
|
|
suggestTarget,
|
|
validateForkMappingTargets,
|
|
} from '@/ee/workspace-forking/lib/mapping/mapping-service'
|
|
import type { ForkResourceCandidate } from '@/ee/workspace-forking/lib/mapping/resources'
|
|
|
|
type ExistingByKind = Partial<Record<ForkRemapKind, Set<string>>>
|
|
|
|
describe('validateForkMappingTargets', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockFilterExisting.mockResolvedValue({} as ExistingByKind)
|
|
mockGetEnvKeys.mockResolvedValue(new Set<string>())
|
|
mockGetCredentialProviders.mockResolvedValue(new Map<string, string | null>())
|
|
})
|
|
|
|
it('rejects a workflow-type entry with a target (identity is system-managed)', async () => {
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'workflow', sourceId: 'wf-src', targetId: 'wf-tgt' },
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
|
|
it('short-circuits without querying when no entry has a target', async () => {
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'env_var', sourceId: 'API_KEY', targetId: null },
|
|
])
|
|
).resolves.toBeUndefined()
|
|
expect(mockFilterExisting).not.toHaveBeenCalled()
|
|
expect(mockGetEnvKeys).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('accepts an env-var whose target key exists in the target workspace', async () => {
|
|
mockGetEnvKeys.mockResolvedValue(new Set(['API_KEY']))
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'API_KEY' },
|
|
])
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('rejects an env-var whose target key is not in the target workspace', async () => {
|
|
mockGetEnvKeys.mockResolvedValue(new Set())
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'env_var', sourceId: 'API_KEY', targetId: 'missing' },
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
|
|
it('accepts a target validated by exact id even when picker lists are capped', async () => {
|
|
// filterExistingForkTargets checks by exact id (cap-free), so a target that would
|
|
// sit past the candidate cap still validates.
|
|
mockFilterExisting.mockResolvedValue({ table: new Set(['table-1001']) })
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'table', sourceId: 'table-src', targetId: 'table-1001' },
|
|
])
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('rejects a target that does not exist in the target workspace', async () => {
|
|
mockFilterExisting.mockResolvedValue({ table: new Set<string>() })
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'table', sourceId: 'table-src', targetId: 'table-gone' },
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
|
|
it('accepts a file target whose storage key exists in the target workspace', async () => {
|
|
// Files are mappable like any other content kind; the target is the storage key, and
|
|
// filterExistingForkTargets resolves file existence by key in the target workspace.
|
|
mockFilterExisting.mockResolvedValue({ file: new Set(['workspace/DST/report.pdf']) })
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{
|
|
resourceType: 'file',
|
|
sourceId: 'workspace/SRC/report.pdf',
|
|
targetId: 'workspace/DST/report.pdf',
|
|
},
|
|
])
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('rejects a file target whose storage key is missing in the target workspace', async () => {
|
|
mockFilterExisting.mockResolvedValue({ file: new Set<string>() })
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{
|
|
resourceType: 'file',
|
|
sourceId: 'workspace/SRC/report.pdf',
|
|
targetId: 'workspace/DST/gone.pdf',
|
|
},
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
|
|
it('rejects a credential whose target provider differs from the source provider', async () => {
|
|
mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) })
|
|
mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) =>
|
|
workspaceId === 'ws-source'
|
|
? new Map([['cred-src', 'google-email']])
|
|
: new Map([['cred-tgt', 'google-calendar']])
|
|
)
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'oauth_credential', sourceId: 'cred-src', targetId: 'cred-tgt' },
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
|
|
it('accepts a credential whose target provider matches the source provider', async () => {
|
|
mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) })
|
|
mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) =>
|
|
workspaceId === 'ws-source'
|
|
? new Map([['cred-src', 'google-email']])
|
|
: new Map([['cred-tgt', 'google-email']])
|
|
)
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'oauth_credential', sourceId: 'cred-src', targetId: 'cred-tgt' },
|
|
])
|
|
).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('rejects a credential whose source is not a credential in the source workspace', async () => {
|
|
mockFilterExisting.mockResolvedValue({ credential: new Set(['cred-tgt']) })
|
|
mockGetCredentialProviders.mockImplementation(async (_db: unknown, workspaceId: string) =>
|
|
workspaceId === 'ws-source'
|
|
? new Map<string, string | null>() // cred-foreign is not in the source
|
|
: new Map([['cred-tgt', 'google-email']])
|
|
)
|
|
await expect(
|
|
validateForkMappingTargets('ws-source', 'ws-target', [
|
|
{ resourceType: 'oauth_credential', sourceId: 'cred-foreign', targetId: 'cred-tgt' },
|
|
])
|
|
).rejects.toBeInstanceOf(ForkError)
|
|
})
|
|
})
|
|
|
|
describe('findDuplicateTargetEntry', () => {
|
|
it('returns null when every target is used by at most one source', () => {
|
|
expect(
|
|
findDuplicateTargetEntry([
|
|
{ resourceType: 'oauth_credential', sourceId: 'c1', targetId: 't1' },
|
|
{ resourceType: 'oauth_credential', sourceId: 'c2', targetId: 't2' },
|
|
])
|
|
).toBeNull()
|
|
})
|
|
|
|
it('flags two distinct sources mapped to the same target', () => {
|
|
expect(
|
|
findDuplicateTargetEntry([
|
|
{ resourceType: 'oauth_credential', sourceId: 'c1', targetId: 'shared' },
|
|
{ resourceType: 'oauth_credential', sourceId: 'c2', targetId: 'shared' },
|
|
])
|
|
).toEqual({ resourceType: 'oauth_credential', targetId: 'shared' })
|
|
})
|
|
|
|
it('ignores cleared (null target) entries', () => {
|
|
expect(
|
|
findDuplicateTargetEntry([
|
|
{ resourceType: 'oauth_credential', sourceId: 'c1', targetId: null },
|
|
{ resourceType: 'oauth_credential', sourceId: 'c2', targetId: null },
|
|
])
|
|
).toBeNull()
|
|
})
|
|
|
|
it('does not flag the same source+target repeated', () => {
|
|
expect(
|
|
findDuplicateTargetEntry([
|
|
{ resourceType: 'table', sourceId: 'c1', targetId: 't1' },
|
|
{ resourceType: 'table', sourceId: 'c1', targetId: 't1' },
|
|
])
|
|
).toBeNull()
|
|
})
|
|
|
|
it('does not conflate the same target id across resource types', () => {
|
|
expect(
|
|
findDuplicateTargetEntry([
|
|
{ resourceType: 'oauth_credential', sourceId: 'c1', targetId: 'same' },
|
|
{ resourceType: 'table', sourceId: 'c2', targetId: 'same' },
|
|
])
|
|
).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('suggestTarget', () => {
|
|
const cand = (id: string, label: string, providerId?: string): ForkResourceCandidate => ({
|
|
id,
|
|
label,
|
|
providerId,
|
|
})
|
|
|
|
it('disambiguates same-name credentials by matching the source provider', () => {
|
|
const target = suggestTarget('credential', 'Work', 'google-email', [
|
|
cand('c1', 'Work', 'google-calendar'),
|
|
cand('c2', 'Work', 'google-email'),
|
|
])
|
|
expect(target).toBe('c2')
|
|
})
|
|
|
|
it('suggests a unique name match for a non-credential kind', () => {
|
|
expect(
|
|
suggestTarget('table', 'Orders', undefined, [cand('t1', 'Orders'), cand('t2', 'Other')])
|
|
).toBe('t1')
|
|
})
|
|
|
|
it('returns null when the name is ambiguous (two same-name candidates)', () => {
|
|
expect(
|
|
suggestTarget('table', 'Dup', undefined, [cand('t1', 'Dup'), cand('t2', 'Dup')])
|
|
).toBeNull()
|
|
})
|
|
|
|
it('returns null when no candidate name matches', () => {
|
|
expect(suggestTarget('table', 'Orders', undefined, [cand('t1', 'Other')])).toBeNull()
|
|
})
|
|
|
|
it('matches the name case- and whitespace-insensitively', () => {
|
|
expect(suggestTarget('table', ' Orders ', undefined, [cand('t1', 'orders')])).toBe('t1')
|
|
})
|
|
})
|