1import {
2 InMemoryService,
3 syncState,
4 type ResourceState,
5 type StateService,
6} from "@/State";
7import { describe, expect, it } from "alchemy-test";
8import * as Effect from "effect/Effect";
9
10describe("syncState", { tags: ["unit", "local"] }, () => {
11 it.effect(
12 "copies source resources and overwrites matching destination resources",
13 () =>
14 Effect.gen(function* () {
15 const sourceA = resource("resource-a", { value: "source-a" });
16 const sourceB = resource("resource-b", { value: "source-b" });
17 const destinationA = resource("resource-a", { value: "destination-a" });
18
19 const source = yield* InMemoryService({
20 app: {
21 dev: {
22 "resource-a": sourceA,
23 "resource-b": sourceB,
24 },
25 },
26 });
27 const destination = yield* InMemoryService({
28 app: {
29 dev: {
30 "resource-a": destinationA,
31 },
32 },
33 });
34
35 yield* syncState(source, destination);
36
37 yield* expectStage(destination, "app", "dev", {
38 "resource-a": sourceA,
39 "resource-b": sourceB,
40 });
41 }),
42 );
43
44 it.effect(
45 "deletes resources from destination when they are absent from source",
46 () =>
47 Effect.gen(function* () {
48 const source = yield* InMemoryService({
49 app: {
50 dev: {
51 "resource-a": resource("resource-a", { value: "source-a" }),
52 },
53 },
54 });
55 const destination = yield* InMemoryService({
56 app: {
57 dev: {
58 "resource-a": resource("resource-a", { value: "destination-a" }),
59 "resource-b": resource("resource-b", { value: "destination-b" }),
60 },
61 prod: {
62 "resource-c": resource("resource-c", { value: "destination-c" }),
63 },
64 },
65 oldApp: {
66 dev: {
67 "resource-d": resource("resource-d", { value: "destination-d" }),
68 },
69 },
70 });
71
72 yield* syncState(source, destination);
73
74 yield* expectStage(destination, "app", "dev", {
75 "resource-a": resource("resource-a", { value: "source-a" }),
76 });
77 yield* expectStage(destination, "app", "prod", {});
78 yield* expectStage(destination, "oldApp", "dev", {});
79 expect(yield* destination.listStacks()).toEqual(["app"]);
80 }),
81 );
82});
83
84const resource = (
85 fqn: string,
86 attr: Record<string, unknown>,
87): ResourceState => ({
88 resourceType: "test:resource",
89 namespace: undefined,
90 fqn,
91 logicalId: fqn,
92 instanceId: `instance-${fqn}`,
93 providerVersion: 1,
94 status: "created",
95 downstream: [],
96 bindings: [],
97 props: {},
98 attr,
99});
100
101const listStage = Effect.fn(function* (
102 state: StateService,
103 stack: string,
104 stage: string,
105) {
106 const fqns = yield* state.list({ stack, stage });
107 const entries = yield* Effect.forEach(
108 fqns,
109 Effect.fn(function* (fqn) {
110 return [fqn, yield* state.get({ stack, stage, fqn })] as const;
111 }),
112 );
113 return Object.fromEntries(entries);
114});
115
116const expectStage = Effect.fn(function* (
117 state: StateService,
118 stack: string,
119 stage: string,
120 expected: Record<string, ResourceState>,
121) {
122 expect(yield* listStage(state, stack, stage)).toEqual(expected);
123});