@a5c-ai/krate 5.0.1-staging.00fa5317c
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/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +278 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +209 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +161 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +221 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +247 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- package/tests/writeback-persistence.test.js +207 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
// Slice 1.1 — Org Scoping Enforcement
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import {
|
|
6
|
+
orgNamespaceName,
|
|
7
|
+
normalizeOrgSlug,
|
|
8
|
+
createResource,
|
|
9
|
+
} from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helper: create a fake resource gateway for controller tests
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function createFakeGateway(overrides = {}) {
|
|
15
|
+
const calls = { applied: [], deleted: [], got: [] };
|
|
16
|
+
return {
|
|
17
|
+
calls,
|
|
18
|
+
gateway: {
|
|
19
|
+
role: 'kubernetes-resource-gateway',
|
|
20
|
+
namespace: 'krate-system',
|
|
21
|
+
resourceDefinitions: [],
|
|
22
|
+
async snapshot() { return { source: 'kubernetes', namespace: 'krate-system', resources: {}, commands: [], events: [], permissions: [], storage: {} }; },
|
|
23
|
+
async list() { return overrides.listResult || { items: [] }; },
|
|
24
|
+
async get(kind, name) { calls.got.push({ kind, name }); return overrides.getResult !== undefined ? overrides.getResult : null; },
|
|
25
|
+
async apply(resource) { calls.applied.push(resource); return { operation: 'apply', resource }; },
|
|
26
|
+
async delete(kind, name) { calls.deleted.push({ kind, name }); return { operation: 'delete', resource: null }; },
|
|
27
|
+
async createRepository(input) { return { operation: 'apply', resource: input }; },
|
|
28
|
+
async createOrganization(input) { return { operation: 'create-organization', organization: { kind: 'Organization', metadata: { name: input.slug || input.name }, spec: { organizationRef: input.slug || input.name, ...input } } }; },
|
|
29
|
+
watch() { return {}; }
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function makeController(overrides = {}, controllerOptions = {}) {
|
|
35
|
+
const { calls, gateway } = createFakeGateway(overrides);
|
|
36
|
+
const { createKrateApiController } = await import('../src/api-controller.js');
|
|
37
|
+
const controller = createKrateApiController({ resourceGateway: gateway, ...controllerOptions });
|
|
38
|
+
return { controller, calls };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Test 1 — Org namespace derivation
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
describe('Slice 1.1 — Org namespace derivation', () => {
|
|
45
|
+
test('orgNamespaceName derives krate-org-<slug> from org slug', () => {
|
|
46
|
+
return import('../src/org-scoping.js').then(({ orgNamespaceName: scopedFn }) => {
|
|
47
|
+
assert.equal(scopedFn('a5c-ai'), 'krate-org-a5c-ai');
|
|
48
|
+
assert.equal(scopedFn('my-org'), 'krate-org-my-org');
|
|
49
|
+
assert.equal(scopedFn('default'), 'krate-org-default');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Test 2 — applyResourceForOrg creates resource in correct namespace
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
describe('Slice 1.1 — applyResource in org scope', () => {
|
|
58
|
+
test('applyResourceForOrg creates resource in the correct org namespace', async () => {
|
|
59
|
+
const { controller, calls } = await makeController();
|
|
60
|
+
|
|
61
|
+
assert.equal(typeof controller.applyResourceForOrg, 'function',
|
|
62
|
+
'controller must expose applyResourceForOrg(orgSlug, resource)');
|
|
63
|
+
|
|
64
|
+
const resource = createResource('Repository', { name: 'my-repo' }, {
|
|
65
|
+
organizationRef: 'a5c-ai',
|
|
66
|
+
visibility: 'internal'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const result = await controller.applyResourceForOrg('a5c-ai', resource);
|
|
70
|
+
|
|
71
|
+
assert.equal(
|
|
72
|
+
result.resource.metadata.namespace,
|
|
73
|
+
'krate-org-a5c-ai',
|
|
74
|
+
'resource must be placed in the org namespace krate-org-a5c-ai'
|
|
75
|
+
);
|
|
76
|
+
assert.equal(calls.applied.length, 1);
|
|
77
|
+
assert.equal(calls.applied[0].metadata.namespace, 'krate-org-a5c-ai');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Test 3 — Cross-org reference denial (krate-org-* namespace mismatch)
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
describe('Slice 1.1 — Cross-org reference denial', () => {
|
|
85
|
+
test('applyResource rejects a resource that references a different org namespace', async () => {
|
|
86
|
+
const { controller, calls } = await makeController();
|
|
87
|
+
|
|
88
|
+
const crossOrgResource = {
|
|
89
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
90
|
+
kind: 'Repository',
|
|
91
|
+
metadata: {
|
|
92
|
+
name: 'stolen-repo',
|
|
93
|
+
namespace: 'krate-org-org-b',
|
|
94
|
+
labels: {}
|
|
95
|
+
},
|
|
96
|
+
spec: {
|
|
97
|
+
organizationRef: 'org-a',
|
|
98
|
+
visibility: 'internal'
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await assert.rejects(
|
|
103
|
+
() => controller.applyResource(crossOrgResource),
|
|
104
|
+
(err) => {
|
|
105
|
+
assert.match(
|
|
106
|
+
err.message,
|
|
107
|
+
/cross.org|org.*mismatch|namespace.*does not match/i,
|
|
108
|
+
'error message must describe the cross-org violation'
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
112
|
+
'applyResource must reject resources with cross-org namespace/organizationRef mismatch'
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
assert.equal(calls.applied.length, 0, 'gateway.apply must not be called for a cross-org resource');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Test 4 — Org-scoped resource listing
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
describe('Slice 1.1 — Org-scoped resource listing', () => {
|
|
123
|
+
test('listResource with org filter returns only resources from that org namespace', async () => {
|
|
124
|
+
const repoOrgA = createResource(
|
|
125
|
+
'Repository',
|
|
126
|
+
{ name: 'repo-alpha', namespace: 'krate-org-org-a' },
|
|
127
|
+
{ organizationRef: 'org-a', visibility: 'internal' }
|
|
128
|
+
);
|
|
129
|
+
const repoOrgB = createResource(
|
|
130
|
+
'Repository',
|
|
131
|
+
{ name: 'repo-beta', namespace: 'krate-org-org-b' },
|
|
132
|
+
{ organizationRef: 'org-b', visibility: 'private' }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const { controller } = await makeController({ listResult: { items: [repoOrgA, repoOrgB] } });
|
|
136
|
+
|
|
137
|
+
assert.equal(typeof controller.listResourceForOrg, 'function',
|
|
138
|
+
'controller must expose listResourceForOrg(org, kind)');
|
|
139
|
+
|
|
140
|
+
const result = await controller.listResourceForOrg('org-a', 'Repository');
|
|
141
|
+
|
|
142
|
+
assert.ok(Array.isArray(result.items), 'result must have an items array');
|
|
143
|
+
assert.equal(result.items.length, 1, 'only org-a resources should be returned');
|
|
144
|
+
assert.equal(result.items[0].metadata.name, 'repo-alpha');
|
|
145
|
+
assert.equal(
|
|
146
|
+
result.items.every((item) => item.spec?.organizationRef === 'org-a'),
|
|
147
|
+
true,
|
|
148
|
+
'every returned item must belong to org-a'
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Test 5 — Org-scoped audit event
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
describe('Slice 1.1 — Org-scoped audit event', () => {
|
|
157
|
+
test('applyResource emits an audit event with org context after a successful apply', async () => {
|
|
158
|
+
const auditEvents = [];
|
|
159
|
+
|
|
160
|
+
const resource = createResource(
|
|
161
|
+
'Repository',
|
|
162
|
+
{ name: 'audited-repo', namespace: 'krate-org-a5c-ai' },
|
|
163
|
+
{ organizationRef: 'a5c-ai', visibility: 'internal' }
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const { controller } = await makeController({}, {
|
|
167
|
+
onAuditEvent: (event) => auditEvents.push(event)
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await controller.applyResource(resource);
|
|
171
|
+
|
|
172
|
+
assert.equal(auditEvents.length, 1, 'exactly one audit event must be emitted per apply');
|
|
173
|
+
|
|
174
|
+
const event = auditEvents[0];
|
|
175
|
+
|
|
176
|
+
assert.ok(event, 'audit event must be emitted');
|
|
177
|
+
assert.equal(event.operation, 'apply', 'event.operation must be "apply"');
|
|
178
|
+
assert.equal(event.org, 'a5c-ai', 'event.org must be the org slug');
|
|
179
|
+
assert.equal(event.namespace, 'krate-org-a5c-ai', 'event.namespace must be the org namespace');
|
|
180
|
+
assert.equal(event.kind, 'Repository', 'event.kind must match the resource kind');
|
|
181
|
+
assert.equal(event.name, 'audited-repo', 'event.name must match the resource name');
|
|
182
|
+
assert.ok(event.timestamp, 'event.timestamp must be present');
|
|
183
|
+
assert.doesNotThrow(() => new Date(event.timestamp).toISOString(),
|
|
184
|
+
'event.timestamp must be a valid ISO date string');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Test 6 — applyResourceForOrg rejects org mismatch in organizationRef
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
describe('Slice 1.1 — applyResourceForOrg org mismatch error path', () => {
|
|
192
|
+
test('applyResourceForOrg rejects when organizationRef does not match target org', async () => {
|
|
193
|
+
const { controller, calls } = await makeController();
|
|
194
|
+
|
|
195
|
+
const resource = createResource('Repository', { name: 'mismatch-repo' }, {
|
|
196
|
+
organizationRef: 'org-a',
|
|
197
|
+
visibility: 'internal'
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await assert.rejects(
|
|
201
|
+
() => controller.applyResourceForOrg('org-b', resource),
|
|
202
|
+
(err) => {
|
|
203
|
+
assert.match(
|
|
204
|
+
err.message,
|
|
205
|
+
/org.*mismatch/i,
|
|
206
|
+
'error must describe the org mismatch'
|
|
207
|
+
);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert.equal(calls.applied.length, 0, 'gateway.apply must not be called on org mismatch');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Test 7 — deleteResourceForOrg happy path
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
describe('Slice 1.1 — deleteResourceForOrg', () => {
|
|
220
|
+
test('deleteResourceForOrg deletes resource in the correct org namespace', async () => {
|
|
221
|
+
const orgResource = {
|
|
222
|
+
resource: createResource('Repository', { name: 'my-repo', namespace: 'krate-org-org-a' }, {
|
|
223
|
+
organizationRef: 'org-a',
|
|
224
|
+
visibility: 'internal'
|
|
225
|
+
})
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const { controller, calls } = await makeController({ getResult: orgResource });
|
|
229
|
+
|
|
230
|
+
assert.equal(typeof controller.deleteResourceForOrg, 'function',
|
|
231
|
+
'controller must expose deleteResourceForOrg(org, kind, name)');
|
|
232
|
+
|
|
233
|
+
await controller.deleteResourceForOrg('org-a', 'Repository', 'my-repo');
|
|
234
|
+
|
|
235
|
+
assert.equal(calls.deleted.length, 1, 'gateway.delete must be called once');
|
|
236
|
+
assert.equal(calls.deleted[0].name, 'my-repo');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Test 8 — deleteResourceForOrg cross-org denial
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
describe('Slice 1.1 — deleteResourceForOrg cross-org denial', () => {
|
|
244
|
+
test('deleteResourceForOrg rejects when resource belongs to a different org', async () => {
|
|
245
|
+
const foreignResource = {
|
|
246
|
+
resource: createResource('Repository', { name: 'foreign-repo', namespace: 'krate-org-org-b' }, {
|
|
247
|
+
organizationRef: 'org-b',
|
|
248
|
+
visibility: 'internal'
|
|
249
|
+
})
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const { controller, calls } = await makeController({ getResult: foreignResource });
|
|
253
|
+
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
() => controller.deleteResourceForOrg('org-a', 'Repository', 'foreign-repo'),
|
|
256
|
+
(err) => {
|
|
257
|
+
assert.match(
|
|
258
|
+
err.message,
|
|
259
|
+
/cross.org|does not match/i,
|
|
260
|
+
'error must describe the cross-org denial'
|
|
261
|
+
);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
assert.equal(calls.deleted.length, 0, 'gateway.delete must not be called for cross-org denial');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// Test 9 — getResourceForOrg happy path
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
describe('Slice 1.1 — getResourceForOrg', () => {
|
|
274
|
+
test('getResourceForOrg returns resource when org matches', async () => {
|
|
275
|
+
const orgResource = {
|
|
276
|
+
resource: createResource('Repository', { name: 'my-repo', namespace: 'krate-org-org-a' }, {
|
|
277
|
+
organizationRef: 'org-a',
|
|
278
|
+
visibility: 'internal'
|
|
279
|
+
})
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const { controller } = await makeController({ getResult: orgResource });
|
|
283
|
+
|
|
284
|
+
assert.equal(typeof controller.getResourceForOrg, 'function',
|
|
285
|
+
'controller must expose getResourceForOrg(org, kind, name)');
|
|
286
|
+
|
|
287
|
+
const result = await controller.getResourceForOrg('org-a', 'Repository', 'my-repo');
|
|
288
|
+
|
|
289
|
+
assert.ok(result, 'result must be returned');
|
|
290
|
+
assert.equal(result.resource.metadata.name, 'my-repo');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// Test 10 — getResourceForOrg cross-org denial
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
describe('Slice 1.1 — getResourceForOrg cross-org denial', () => {
|
|
298
|
+
test('getResourceForOrg rejects when resource belongs to a different org', async () => {
|
|
299
|
+
const foreignResource = {
|
|
300
|
+
resource: createResource('Repository', { name: 'stolen-repo', namespace: 'krate-org-org-b' }, {
|
|
301
|
+
organizationRef: 'org-b',
|
|
302
|
+
visibility: 'internal'
|
|
303
|
+
})
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const { controller } = await makeController({ getResult: foreignResource });
|
|
307
|
+
|
|
308
|
+
await assert.rejects(
|
|
309
|
+
() => controller.getResourceForOrg('org-a', 'Repository', 'stolen-repo'),
|
|
310
|
+
(err) => {
|
|
311
|
+
assert.match(
|
|
312
|
+
err.message,
|
|
313
|
+
/cross.org|does not match/i,
|
|
314
|
+
'error must describe the cross-org denial'
|
|
315
|
+
);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Test 11 — createRepository emits audit event
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
describe('Slice 1.1 — createRepository audit', () => {
|
|
326
|
+
test('createRepository emits an audit event', async () => {
|
|
327
|
+
const auditEvents = [];
|
|
328
|
+
|
|
329
|
+
const { controller } = await makeController({}, {
|
|
330
|
+
onAuditEvent: (event) => auditEvents.push(event)
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await controller.createRepository({
|
|
334
|
+
name: 'new-repo',
|
|
335
|
+
organizationRef: 'a5c-ai',
|
|
336
|
+
visibility: 'internal'
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
|
|
340
|
+
const event = auditEvents.find((e) => e.operation === 'create-repository');
|
|
341
|
+
assert.ok(event, 'audit event with operation "create-repository" must exist');
|
|
342
|
+
assert.ok(event.timestamp, 'event.timestamp must be present');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Test 12 — createOrganization emits audit event
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
describe('Slice 1.1 — createOrganization audit', () => {
|
|
350
|
+
test('createOrganization emits an audit event', async () => {
|
|
351
|
+
const auditEvents = [];
|
|
352
|
+
|
|
353
|
+
const { controller } = await makeController({}, {
|
|
354
|
+
onAuditEvent: (event) => auditEvents.push(event)
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await controller.createOrganization({
|
|
358
|
+
slug: 'new-org',
|
|
359
|
+
displayName: 'New Org'
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
|
|
363
|
+
const event = auditEvents.find((e) => e.operation === 'create-organization');
|
|
364
|
+
assert.ok(event, 'audit event with operation "create-organization" must exist');
|
|
365
|
+
assert.ok(event.timestamp, 'event.timestamp must be present');
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Test 13 — Audit callback error resilience
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
describe('Slice 1.1 — Audit callback error resilience', () => {
|
|
373
|
+
test('audit callback error does not crash apply operations', async () => {
|
|
374
|
+
const { controller } = await makeController({}, {
|
|
375
|
+
onAuditEvent: () => { throw new Error('audit system down'); }
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const resource = createResource('Repository', { name: 'resilient-repo', namespace: 'krate-org-a5c-ai' }, {
|
|
379
|
+
organizationRef: 'a5c-ai',
|
|
380
|
+
visibility: 'internal'
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// This must not throw despite the audit callback throwing
|
|
384
|
+
const result = await controller.applyResource(resource);
|
|
385
|
+
assert.ok(result, 'apply must succeed even when audit callback throws');
|
|
386
|
+
assert.equal(result.operation, 'apply');
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Test 14 — applyResourceForOrg normalizes un-normalized slug
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
describe('Slice 1.1 — applyResourceForOrg slug normalization', () => {
|
|
394
|
+
test('applyResourceForOrg with un-normalized slug ("Org-A") normalizes the stored organizationRef to "org-a"', async () => {
|
|
395
|
+
const { controller, calls } = await makeController();
|
|
396
|
+
|
|
397
|
+
const resource = createResource('Repository', { name: 'norm-repo' }, {
|
|
398
|
+
visibility: 'internal'
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const result = await controller.applyResourceForOrg('Org-A', resource);
|
|
402
|
+
|
|
403
|
+
assert.equal(result.resource.spec.organizationRef, 'org-a',
|
|
404
|
+
'organizationRef must be normalized to lowercase');
|
|
405
|
+
assert.equal(result.resource.metadata.namespace, 'krate-org-org-a',
|
|
406
|
+
'namespace must use the normalized slug');
|
|
407
|
+
assert.equal(calls.applied.length, 1);
|
|
408
|
+
assert.equal(calls.applied[0].spec.organizationRef, 'org-a');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
// Test 15 — deleteResourceForOrg when resource has no namespace
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
describe('Slice 1.1 — deleteResourceForOrg no-namespace denial', () => {
|
|
416
|
+
test('deleteResourceForOrg when resource has no namespace throws cross-org error', async () => {
|
|
417
|
+
const noNsResource = {
|
|
418
|
+
resource: createResource('Repository', { name: 'no-ns-repo' }, {
|
|
419
|
+
organizationRef: 'org-a',
|
|
420
|
+
visibility: 'internal'
|
|
421
|
+
})
|
|
422
|
+
};
|
|
423
|
+
// Ensure metadata.namespace is absent
|
|
424
|
+
delete noNsResource.resource.metadata.namespace;
|
|
425
|
+
|
|
426
|
+
const { controller, calls } = await makeController({ getResult: noNsResource });
|
|
427
|
+
|
|
428
|
+
await assert.rejects(
|
|
429
|
+
() => controller.deleteResourceForOrg('org-a', 'Repository', 'no-ns-repo'),
|
|
430
|
+
(err) => {
|
|
431
|
+
assert.match(err.message, /cross.org|does not match/i,
|
|
432
|
+
'error must describe cross-org denial for absent namespace');
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
assert.equal(calls.deleted.length, 0, 'gateway.delete must not be called');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Test 16 — getResourceForOrg when resource has no namespace
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
describe('Slice 1.1 — getResourceForOrg no-namespace denial', () => {
|
|
445
|
+
test('getResourceForOrg when resource has no namespace throws cross-org error', async () => {
|
|
446
|
+
const noNsResource = {
|
|
447
|
+
resource: createResource('Repository', { name: 'no-ns-get-repo' }, {
|
|
448
|
+
organizationRef: 'org-a',
|
|
449
|
+
visibility: 'internal'
|
|
450
|
+
})
|
|
451
|
+
};
|
|
452
|
+
delete noNsResource.resource.metadata.namespace;
|
|
453
|
+
|
|
454
|
+
const { controller } = await makeController({ getResult: noNsResource });
|
|
455
|
+
|
|
456
|
+
await assert.rejects(
|
|
457
|
+
() => controller.getResourceForOrg('org-a', 'Repository', 'no-ns-get-repo'),
|
|
458
|
+
(err) => {
|
|
459
|
+
assert.match(err.message, /cross.org|does not match/i,
|
|
460
|
+
'error must describe cross-org denial for absent namespace');
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// Test 17 — applyResource with organizationRef but no namespace auto-assigns
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
describe('Slice 1.1 — applyResource auto-assigns namespace from organizationRef', () => {
|
|
471
|
+
test('applyResource with organizationRef but no namespace auto-assigns namespace', async () => {
|
|
472
|
+
const { controller, calls } = await makeController();
|
|
473
|
+
|
|
474
|
+
const resource = {
|
|
475
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
476
|
+
kind: 'Repository',
|
|
477
|
+
metadata: {
|
|
478
|
+
name: 'auto-ns-repo',
|
|
479
|
+
labels: {}
|
|
480
|
+
},
|
|
481
|
+
spec: {
|
|
482
|
+
organizationRef: 'org-a',
|
|
483
|
+
visibility: 'internal'
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const result = await controller.applyResource(resource);
|
|
488
|
+
|
|
489
|
+
assert.ok(result, 'apply must succeed');
|
|
490
|
+
assert.equal(calls.applied.length, 1);
|
|
491
|
+
assert.equal(calls.applied[0].metadata.namespace, 'krate-org-org-a',
|
|
492
|
+
'namespace must be auto-assigned from organizationRef');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// Test 18 — deleteResourceForOrg when resource not found returns gracefully
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
describe('Slice 1.1 — deleteResourceForOrg not found', () => {
|
|
500
|
+
test('deleteResourceForOrg when resource not found returns gracefully', async () => {
|
|
501
|
+
// getResult = null means resource not found
|
|
502
|
+
const { controller, calls } = await makeController({ getResult: null });
|
|
503
|
+
|
|
504
|
+
const result = await controller.deleteResourceForOrg('org-a', 'Repository', 'nonexistent-repo');
|
|
505
|
+
|
|
506
|
+
assert.ok(result, 'delete must return a result even when resource is not found');
|
|
507
|
+
assert.equal(calls.deleted.length, 1, 'gateway.delete must still be called');
|
|
508
|
+
assert.equal(calls.deleted[0].name, 'nonexistent-repo');
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Test 19 — normalizeOrgSlug collision detection note
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
describe('Slice 1.1 — normalizeOrgSlug collision detection', () => {
|
|
516
|
+
test('normalizeOrgSlug collision: org-a-b and org_a_b normalize identically', () => {
|
|
517
|
+
// This test documents that underscores and hyphens collapse to the same slug.
|
|
518
|
+
// Real production code should add collision detection when creating organizations.
|
|
519
|
+
assert.equal(normalizeOrgSlug('org-a-b'), normalizeOrgSlug('org_a_b'),
|
|
520
|
+
'org-a-b and org_a_b must normalize to the same slug (collision)');
|
|
521
|
+
assert.equal(normalizeOrgSlug('org_a_b'), 'org-a-b',
|
|
522
|
+
'underscores must be replaced by hyphens in normalized form');
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// Test 20 — normalizeOrgSlug edge cases
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
describe('Slice 1.1 — normalizeOrgSlug edge cases', () => {
|
|
530
|
+
test('normalizeOrgSlug handles empty string', () => {
|
|
531
|
+
assert.equal(normalizeOrgSlug(''), '');
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test('normalizeOrgSlug handles null/undefined', () => {
|
|
535
|
+
assert.equal(normalizeOrgSlug(null), '');
|
|
536
|
+
assert.equal(normalizeOrgSlug(undefined), '');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('normalizeOrgSlug lowercases and strips special chars', () => {
|
|
540
|
+
assert.equal(normalizeOrgSlug('My_Org!@#'), 'my-org');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
test('normalizeOrgSlug strips leading and trailing hyphens', () => {
|
|
544
|
+
assert.equal(normalizeOrgSlug('--my-org--'), 'my-org');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('normalizeOrgSlug truncates to 63 chars', () => {
|
|
548
|
+
const longSlug = 'a'.repeat(100);
|
|
549
|
+
assert.ok(normalizeOrgSlug(longSlug).length <= 63);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
// Test 15 — listResourceForOrg with empty results
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
describe('Slice 1.1 — listResourceForOrg empty results', () => {
|
|
557
|
+
test('listResourceForOrg returns empty items when no resources match', async () => {
|
|
558
|
+
const repoOrgB = createResource(
|
|
559
|
+
'Repository',
|
|
560
|
+
{ name: 'repo-beta', namespace: 'krate-org-org-b' },
|
|
561
|
+
{ organizationRef: 'org-b', visibility: 'private' }
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const { controller } = await makeController({ listResult: { items: [repoOrgB] } });
|
|
565
|
+
|
|
566
|
+
const result = await controller.listResourceForOrg('org-a', 'Repository');
|
|
567
|
+
|
|
568
|
+
assert.ok(Array.isArray(result.items), 'result must have an items array');
|
|
569
|
+
assert.equal(result.items.length, 0, 'no items should match org-a');
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
// Test 16 — listResourceForOrg multi-org filtering
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
describe('Slice 1.1 — listResourceForOrg multi-org filtering', () => {
|
|
577
|
+
test('listResourceForOrg correctly filters across multiple orgs', async () => {
|
|
578
|
+
const repoA1 = createResource('Repository', { name: 'a-repo-1', namespace: 'krate-org-org-a' }, { organizationRef: 'org-a', visibility: 'internal' });
|
|
579
|
+
const repoA2 = createResource('Repository', { name: 'a-repo-2', namespace: 'krate-org-org-a' }, { organizationRef: 'org-a', visibility: 'internal' });
|
|
580
|
+
const repoB1 = createResource('Repository', { name: 'b-repo-1', namespace: 'krate-org-org-b' }, { organizationRef: 'org-b', visibility: 'private' });
|
|
581
|
+
const repoC1 = createResource('Repository', { name: 'c-repo-1', namespace: 'krate-org-org-c' }, { organizationRef: 'org-c', visibility: 'public' });
|
|
582
|
+
|
|
583
|
+
const { controller } = await makeController({ listResult: { items: [repoA1, repoA2, repoB1, repoC1] } });
|
|
584
|
+
|
|
585
|
+
const resultA = await controller.listResourceForOrg('org-a', 'Repository');
|
|
586
|
+
assert.equal(resultA.items.length, 2, 'org-a should have 2 repos');
|
|
587
|
+
assert.deepEqual(resultA.items.map((i) => i.metadata.name).sort(), ['a-repo-1', 'a-repo-2']);
|
|
588
|
+
|
|
589
|
+
const resultB = await controller.listResourceForOrg('org-b', 'Repository');
|
|
590
|
+
assert.equal(resultB.items.length, 1, 'org-b should have 1 repo');
|
|
591
|
+
assert.equal(resultB.items[0].metadata.name, 'b-repo-1');
|
|
592
|
+
|
|
593
|
+
const resultC = await controller.listResourceForOrg('org-c', 'Repository');
|
|
594
|
+
assert.equal(resultC.items.length, 1, 'org-c should have 1 repo');
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// Test 17 — deleteResource emits audit event
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
describe('Slice 1.1 — deleteResource audit', () => {
|
|
602
|
+
test('deleteResource emits an audit event', async () => {
|
|
603
|
+
const auditEvents = [];
|
|
604
|
+
|
|
605
|
+
const { controller } = await makeController({}, {
|
|
606
|
+
onAuditEvent: (event) => auditEvents.push(event)
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
await controller.deleteResource('Repository', 'some-repo');
|
|
610
|
+
|
|
611
|
+
assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
|
|
612
|
+
const event = auditEvents.find((e) => e.operation === 'delete');
|
|
613
|
+
assert.ok(event, 'audit event with operation "delete" must exist');
|
|
614
|
+
assert.equal(event.name, 'some-repo');
|
|
615
|
+
assert.ok(event.timestamp, 'event.timestamp must be present');
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// Test 18 — Cross-org denial for system namespaces
|
|
621
|
+
// (Regression: resources targeting system namespaces like krate-system
|
|
622
|
+
// with any organizationRef must also be blocked.)
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
describe('Slice 1.1 — Cross-org denial for system namespaces', () => {
|
|
625
|
+
test('applyResource rejects resource targeting krate-system with an organizationRef', async () => {
|
|
626
|
+
const { controller, calls } = await makeController();
|
|
627
|
+
|
|
628
|
+
const systemNsResource = {
|
|
629
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
630
|
+
kind: 'Repository',
|
|
631
|
+
metadata: {
|
|
632
|
+
name: 'sneaky-repo',
|
|
633
|
+
namespace: 'krate-system',
|
|
634
|
+
labels: {}
|
|
635
|
+
},
|
|
636
|
+
spec: {
|
|
637
|
+
organizationRef: 'org-a',
|
|
638
|
+
visibility: 'internal'
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
await assert.rejects(
|
|
643
|
+
() => controller.applyResource(systemNsResource),
|
|
644
|
+
(err) => {
|
|
645
|
+
assert.match(
|
|
646
|
+
err.message,
|
|
647
|
+
/cross.org|org.*mismatch|namespace.*does not match/i,
|
|
648
|
+
'error message must describe the namespace/org mismatch'
|
|
649
|
+
);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
assert.equal(calls.applied.length, 0, 'gateway.apply must not be called');
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test('applyResource rejects resource targeting default namespace with an organizationRef', async () => {
|
|
658
|
+
const { controller, calls } = await makeController();
|
|
659
|
+
|
|
660
|
+
const defaultNsResource = {
|
|
661
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
662
|
+
kind: 'Repository',
|
|
663
|
+
metadata: {
|
|
664
|
+
name: 'sneaky-repo-2',
|
|
665
|
+
namespace: 'default',
|
|
666
|
+
labels: {}
|
|
667
|
+
},
|
|
668
|
+
spec: {
|
|
669
|
+
organizationRef: 'org-a',
|
|
670
|
+
visibility: 'internal'
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
await assert.rejects(
|
|
675
|
+
() => controller.applyResource(defaultNsResource),
|
|
676
|
+
(err) => {
|
|
677
|
+
assert.match(
|
|
678
|
+
err.message,
|
|
679
|
+
/cross.org|org.*mismatch|namespace.*does not match/i
|
|
680
|
+
);
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
assert.equal(calls.applied.length, 0);
|
|
686
|
+
});
|
|
687
|
+
});
|