| ... | @@ -38,6 +38,7 @@ import { | ... | @@ -38,6 +38,7 @@ import { |
| 38 | decodeConfirmedOrigin, | 38 | decodeConfirmedOrigin, |
| 39 | decodeRuntimeConfig, | 39 | decodeRuntimeConfig, |
| 40 | RELAY_ENVIRONMENT_CREDENTIAL_SECRET, | 40 | RELAY_ENVIRONMENT_CREDENTIAL_SECRET, |
| | 41 | RELAY_ISSUER_SECRET, |
| 41 | RELAY_URL_SECRET, | 42 | RELAY_URL_SECRET, |
| 42 | } from "./config.ts"; | 43 | } from "./config.ts"; |
| 43 | import * as CloudLink from "./CloudLink.ts"; | 44 | import * as CloudLink from "./CloudLink.ts"; |
| ... | @@ -1158,4 +1159,59 @@ describe("releaseManagedTunnelOnShutdown", () => { | ... | @@ -1158,4 +1159,59 @@ describe("releaseManagedTunnelOnShutdown", () => { |
| 1158 | }), | 1159 | }), |
| 1159 | ); | 1160 | ); |
| 1160 | }); | 1161 | }); |
| | 1162 | |
| | 1163 | // The relay commits the link before the server stores it (#11898). A write |
| | 1164 | // that fails part-way must not leave the stale tunnel config that reports |
| | 1165 | // the link as managed, or Settings never relinks. |
| | 1166 | it.effect("does not report a managed tunnel after a relay config write fails part-way", () => { |
| | 1167 | const { store, values } = makeMemorySecretStore([ |
| | 1168 | [CLOUD_ENDPOINT_RUNTIME_CONFIG, "managed-config"], |
| | 1169 | [CLOUD_ENDPOINT_CONFIRMED_ORIGIN, "confirmed-origin"], |
| | 1170 | [RELAY_URL_SECRET, "https://relay.example.test"], |
| | 1171 | [CLOUD_LINKED_USER_ID, "user-123"], |
| | 1172 | [RELAY_ENVIRONMENT_CREDENTIAL_SECRET, "old-credential"], |
| | 1173 | ]); |
| | 1174 | const failingStore: ServerSecretStore.ServerSecretStore["Service"] = { |
| | 1175 | ...store, |
| | 1176 | set: (name, value) => |
| | 1177 | name === RELAY_ISSUER_SECRET |
| | 1178 | ? Effect.fail( |
| | 1179 | new ServerSecretStore.SecretStorePersistError({ |
| | 1180 | resource: name, |
| | 1181 | cause: PlatformError.systemError({ |
| | 1182 | _tag: "PermissionDenied", |
| | 1183 | module: "FileSystem", |
| | 1184 | method: "rename", |
| | 1185 | pathOrDescriptor: `${name}.bin`, |
| | 1186 | }), |
| | 1187 | }), |
| | 1188 | ) |
| | 1189 | : store.set(name, value), |
| | 1190 | }; |
| | 1191 | const applyConfigCalls: Array<unknown> = []; |
| | 1192 | const requests: Array<HttpClientRequest.HttpClientRequest> = []; |
| | 1193 | const cloudMintPublicKey = |
| | 1194 | "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAMY09Vo5CzEA6aEkhB/0h27t4Qs71SHS1fTSI4K0KvZQ=\n-----END PUBLIC KEY-----\n"; |
| | 1195 | |
| | 1196 | return Effect.gen(function* () { |
| | 1197 | const link = yield* CloudLink.CloudLink; |
| | 1198 | const error = yield* Effect.flip( |
| | 1199 | link.applyRelayConfig({ |
| | 1200 | relayUrl: "https://relay.example.test", |
| | 1201 | cloudUserId: "user-123", |
| | 1202 | environmentCredential: "new-credential", |
| | 1203 | cloudMintPublicKey, |
| | 1204 | endpointRuntime: null, |
| | 1205 | }), |
| | 1206 | ); |
| | 1207 | |
| | 1208 | expect(error._tag).toBe("CloudLinkInternalError"); |
| | 1209 | expect(yield* link.linkState()).toMatchObject({ |
| | 1210 | linked: true, |
| | 1211 | cloudUserId: "user-123", |
| | 1212 | managedTunnelActive: false, |
| | 1213 | }); |
| | 1214 | expect(values.has(CLOUD_ENDPOINT_RUNTIME_CONFIG)).toBe(false); |
| | 1215 | }).pipe(provideReleaseHarness({ store: failingStore, applyConfigCalls, requests })); |
| | 1216 | }); |
| 1161 | }); | 1217 | }); |