| 1 | import { |
| 2 | InMemoryService, |
| 3 | syncState, |
| 4 | type ResourceState, |
| 5 | type StateService, |
| 6 | } from "@/State"; |
| 7 | import { describe, expect, it } from "alchemy-test"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | |
| 10 | describe("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 | |
| 84 | const 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 | |
| 101 | const 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 | |
| 116 | const 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 | }); |