diff --git a/tests/apiExecutor.test.ts b/tests/apiExecutor.test.ts index ee175c2..1eae891 100644 --- a/tests/apiExecutor.test.ts +++ b/tests/apiExecutor.test.ts @@ -1,12 +1,11 @@ import nock from "nock"; - +import { CredentialsMethod } from "../credentials"; import { - OpenFgaClient, - UserClientConfigurationParams, FgaApiNotFoundError, FgaApiValidationError, + OpenFgaClient, + type UserClientConfigurationParams, } from "../index"; -import { CredentialsMethod } from "../credentials"; import { baseConfig, defaultConfiguration, OPENFGA_STORE_ID } from "./helpers/default-config"; describe("OpenFgaClient.executeApiRequest", () => { @@ -16,21 +15,6 @@ describe("OpenFgaClient.executeApiRequest", () => { credentials: { method: CredentialsMethod.None } }; - beforeAll(() => { - nock.restore(); - nock.cleanAll(); - nock.activate(); - nock.disableNetConnect(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - afterAll(() => { - nock.restore(); - }); - describe("GET requests", () => { it("should make GET requests successfully", async () => { const fgaClient = new OpenFgaClient(testConfig); @@ -337,21 +321,6 @@ describe("OpenFgaClient.executeApiRequest - path parameters", () => { credentials: { method: CredentialsMethod.None } }; - beforeAll(() => { - nock.restore(); - nock.cleanAll(); - nock.activate(); - nock.disableNetConnect(); - }); - - afterEach(() => { - nock.cleanAll(); - }); - - afterAll(() => { - nock.restore(); - }); - describe("path parameter replacement", () => { it("should replace path parameters with values (single_parameter)", async () => { const fgaClient = new OpenFgaClient(testConfig); diff --git a/tests/client.test.ts b/tests/client.test.ts index f0b1b16..546f0a5 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -27,10 +27,6 @@ describe("OpenFGA Client", () => { fgaClient = new OpenFgaClient({ ...baseConfig, credentials: { method: CredentialsMethod.None } }); }); - afterEach(() => { - nock.cleanAll(); - }); - describe("Configuration", () => { it("should throw an error if the storeId is not in a valid format", async () => { expect( @@ -81,7 +77,7 @@ describe("OpenFGA Client", () => { describe("ListStores", () => { it("should properly call the ListStores API", async () => { const store = { id: "some-id", name: "some-name" }; - const scope = nocks.listStores(defaultConfiguration.getBasePath(), { + nocks.listStores(defaultConfiguration.getBasePath(), { continuation_token: "", stores: [{ ...store, @@ -91,17 +87,15 @@ describe("OpenFGA Client", () => { }], }); - expect(scope.isDone()).toBe(false); const response = await fgaClient.listStores(); - expect(scope.isDone()).toBe(true); expect(response.stores).toHaveLength(1); expect(response.stores?.[0]).toMatchObject(store); }); it("should properly call the ListStores API with name filter", async () => { const store = { id: "some-id", name: "test-store" }; - const scope = nocks.listStores(defaultConfiguration.getBasePath(), { + nocks.listStores(defaultConfiguration.getBasePath(), { continuation_token: "", stores: [{ ...store, @@ -111,10 +105,8 @@ describe("OpenFGA Client", () => { }], }, 200, { name: "test-store" }); - expect(scope.isDone()).toBe(false); const response = await fgaClient.listStores({ name: "test-store" }); - expect(scope.isDone()).toBe(true); expect(response.stores).toHaveLength(1); expect(response.stores?.[0]).toMatchObject(store); }); @@ -123,16 +115,14 @@ describe("OpenFGA Client", () => { describe("CreateStore", () => { it("should create a store", async () => { const store = { id: "some-id", name: "some-name" }; - const scope = nocks.createStore(defaultConfiguration.getBasePath(), { + nocks.createStore(defaultConfiguration.getBasePath(), { ...store, created_at: "2023-11-02T15:27:47.951Z", updated_at: "2023-11-02T15:27:47.951Z", }); - expect(scope.isDone()).toBe(false); const response = await fgaClient.createStore(store); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject(store); }); }); @@ -140,16 +130,14 @@ describe("OpenFGA Client", () => { describe("GetStore", () => { it("should properly call the GetStore API", async () => { const store = { id: defaultConfiguration.storeId!, name: "some-name" }; - const scope = nocks.getStore(store.id, defaultConfiguration.getBasePath(), { + nocks.getStore(store.id, defaultConfiguration.getBasePath(), { ...store, created_at: "2023-11-02T15:27:47.951Z", updated_at: "2023-11-02T15:27:47.951Z", }); - expect(scope.isDone()).toBe(false); const response = await fgaClient.getStore(); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject(store); }); @@ -159,40 +147,34 @@ describe("OpenFGA Client", () => { const overriddenStoreId = "01HWD53SDGYRXHBXTYA10PF6T4"; const store = { id: overriddenStoreId, name: "some-name" }; - const scope = nocks.getStore(store.id, defaultConfiguration.getBasePath(), { + nocks.getStore(store.id, defaultConfiguration.getBasePath(), { ...store, created_at: "2023-11-02T15:27:47.951Z", updated_at: "2023-11-02T15:27:47.951Z", }); - expect(scope.isDone()).toBe(false); const response = await fgaClient.getStore({ storeId: overriddenStoreId }); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject(store); }); }); describe("DeleteStore", () => { it("should properly call the DeleteStore API", async () => { - const scope = nocks.deleteStore(defaultConfiguration.storeId!); + nocks.deleteStore(defaultConfiguration.storeId!); - expect(scope.isDone()).toBe(false); await fgaClient.deleteStore(); - expect(scope.isDone()).toBe(true); }); }); /* Authorization Models */ describe("ReadAuthorizationModels", () => { it("should properly call the ReadAuthorizationModels API", async () => { - const scope = nocks.readAuthorizationModels(defaultConfiguration.storeId!); + nocks.readAuthorizationModels(defaultConfiguration.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaClient.readAuthorizationModels(); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_models: expect.arrayContaining([]), }); @@ -207,17 +189,15 @@ describe("OpenFGA Client", () => { { type: "workspace", relations: { admin: { this: {} } } }, ], }; - const scope = nocks.writeAuthorizationModel( + nocks.writeAuthorizationModel( baseConfig.storeId!, authorizationModel ); - expect(scope.isDone()).toBe(false); const data = await fgaClient.writeAuthorizationModel( authorizationModel ); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ id: expect.any(String) }); }); }); @@ -225,12 +205,10 @@ describe("OpenFGA Client", () => { describe("ReadAuthorizationModel", () => { it("should properly call the ReadAuthorizationModel API", async () => { const modelId = "01H0THVNGCSAZ6SAQVTHPH3F0Q"; - const scope = nocks.readSingleAuthzModel(defaultConfiguration.storeId!, modelId); + nocks.readSingleAuthzModel(defaultConfiguration.storeId!, modelId); - expect(scope.isDone()).toBe(false); const data = await fgaClient.readAuthorizationModel({ authorizationModelId: modelId }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_model: { id: expect.any(String), @@ -244,17 +222,15 @@ describe("OpenFGA Client", () => { describe("ReadLatestAuthorizationModel", () => { it("should properly call the ReadLatestAuthorizationModel API", async () => { const modelId = "01H0THVNGCSAZ6SAQVTHPH3F0Q"; - const scope = nock(defaultConfiguration.getBasePath()) + nock(defaultConfiguration.getBasePath()) .get(`/stores/${defaultConfiguration.storeId!}/authorization-models`) .query({ page_size: 1 }) .reply(200, { authorization_models: [{ id: modelId, schema_version: "1.1", type_definitions: [] }], }); - expect(scope.isDone()).toBe(false); const data = await fgaClient.readLatestAuthorizationModel(); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_model: { id: expect.any(String), @@ -273,12 +249,10 @@ describe("OpenFGA Client", () => { const startTime = "2022-01-01T00:00:00Z"; const continuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="; - const scope = nocks.readChanges(baseConfig.storeId!, type, pageSize, continuationToken, startTime); + nocks.readChanges(baseConfig.storeId!, type, pageSize, continuationToken, startTime); - expect(scope.isDone()).toBe(false); const response = await fgaClient.readChanges({ type, startTime }, { pageSize, continuationToken }); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject({ changes: expect.arrayContaining([]) }); }); @@ -287,12 +261,10 @@ describe("OpenFGA Client", () => { const continuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="; const startTime = "2022-01-01T00:00:00Z"; - const scope = nocks.readChanges(baseConfig.storeId!, "", pageSize, continuationToken,""); + nocks.readChanges(baseConfig.storeId!, "", pageSize, continuationToken,""); - expect(scope.isDone()).toBe(false); const response = await fgaClient.readChanges(undefined, { pageSize, continuationToken }); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject({ changes: expect.arrayContaining([]) }); }); }); @@ -304,12 +276,10 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.read(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); + nocks.read(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); - expect(scope.isDone()).toBe(false); const data = await fgaClient.read(tuple, { consistency: ConsistencyPreference.HigherConsistency}); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -321,16 +291,14 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.write(baseConfig.storeId!); + nocks.write(baseConfig.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaClient.write({ writes: [tuple], }, { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); @@ -365,6 +333,9 @@ describe("OpenFGA Client", () => { expect(scope2.isDone()).toBe(false); expect(scope3.isDone()).toBe(false); expect(data).toMatchObject({}); + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); it("should not fail the request on errors in non-transaction mode", async () => { @@ -409,6 +380,9 @@ describe("OpenFGA Client", () => { expect(data.writes.find(tuple => tuple.tuple_key.object === tuples[0].object)?.status).toBe(ClientWriteStatus.SUCCESS); expect(data.writes.find(tuple => tuple.tuple_key.object === tuples[1].object)?.status).toBe(ClientWriteStatus.SUCCESS); expect(data.writes.find(tuple => tuple.tuple_key.object === tuples[2].object)?.status).toBe(ClientWriteStatus.FAILURE); + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); it("should throw an error if auth fails in not transaction mode", async () => { @@ -441,6 +415,9 @@ describe("OpenFGA Client", () => { expect(scope0.isDone()).toBe(true); expect(scope1.isDone()).toBe(false); } + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); it("should properly call the Write API when providing one empty array", async () => { @@ -449,9 +426,8 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.write(baseConfig.storeId!); + nocks.write(baseConfig.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaClient.write({ writes: [tuple], deletes: [] @@ -459,7 +435,6 @@ describe("OpenFGA Client", () => { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", }); - expect(scope.isDone()).toBe(true); expect(data.writes.length).toBe(1); expect(data.deletes.length).toBe(0); }); @@ -1091,14 +1066,12 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.write(baseConfig.storeId!); + nocks.write(baseConfig.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaClient.writeTuples([tuple], { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); @@ -1193,14 +1166,12 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.write(baseConfig.storeId!); + nocks.write(baseConfig.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaClient.deleteTuples([tuple], { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); @@ -1296,12 +1267,10 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.check(baseConfig.storeId!, tuple, undefined, undefined, undefined, ConsistencyPreference.HigherConsistency); + nocks.check(baseConfig.storeId!, tuple, undefined, undefined, undefined, ConsistencyPreference.HigherConsistency); - expect(scope.isDone()).toBe(false); const data = await fgaClient.check(tuple, { consistency: ConsistencyPreference.HigherConsistency }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ allowed: expect.any(Boolean) }); }); }); @@ -1349,6 +1318,9 @@ describe("OpenFGA Client", () => { { _request: tuples[1], allowed: false }, { _request: tuples[2], error: expect.any(Error) }, ])); + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); }); @@ -1394,9 +1366,8 @@ describe("OpenFGA Client", () => { }, }; - const scope = nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); + nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); - expect(scope.isDone()).toBe(false); const response = await fgaClient.batchCheck({ checks: [{ user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", @@ -1427,7 +1398,6 @@ describe("OpenFGA Client", () => { consistency: ConsistencyPreference.HigherConsistency, }); - expect(scope.isDone()).toBe(true); expect(response.result).toHaveLength(2); expect(response.result[0].allowed).toBe(true); expect(response.result[1].allowed).toBe(false); @@ -1457,11 +1427,8 @@ describe("OpenFGA Client", () => { }, }; - const scope0 = nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse0, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); - const scope1 = nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse1, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); - - expect(scope0.isDone()).toBe(false); - expect(scope1.isDone()).toBe(false); + nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse0, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); + nocks.singleBatchCheck(baseConfig.storeId!, mockedResponse1, undefined, ConsistencyPreference.HigherConsistency, "01GAHCE4YVKPQEKZQHT2R89MQV").matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); const response = await fgaClient.batchCheck({ checks: [{ @@ -1500,8 +1467,6 @@ describe("OpenFGA Client", () => { maxBatchSize: 2, }); - expect(scope0.isDone()).toBe(true); - expect(scope1.isDone()).toBe(true); expect(response.result).toHaveLength(3); const resp0 = response.result.find(r => r.correlationId === "cor-1"); @@ -1526,9 +1491,9 @@ describe("OpenFGA Client", () => { expect(resp2?.error?.input_error).toBe(ErrorCode.RelationNotFound); expect(resp2?.error?.message).toBe("relation not found"); }); - it("should throw an error if auth fails", async () => { - const scope = nock(defaultConfiguration.getBasePath()) + it("should throw an error if auth fails", async () => { + nock(defaultConfiguration.getBasePath()) .post(`/stores/${baseConfig.storeId!}/batch-check`) .reply(401, {}); @@ -1542,8 +1507,6 @@ describe("OpenFGA Client", () => { }); } catch (err) { expect(err).toBeInstanceOf(FgaApiAuthenticationError); - } finally { - expect(scope.isDone()).toBe(true); } }); @@ -1561,7 +1524,7 @@ describe("OpenFGA Client", () => { }, }; - const scope = nocks + nocks .singleBatchCheck( baseConfig.storeId!, mockedResponse, @@ -1571,7 +1534,6 @@ describe("OpenFGA Client", () => { ) .matchHeader("X-OpenFGA-Client-Bulk-Request-Id", /.*/); - expect(scope.isDone()).toBe(false); const response = await fgaClient.batchCheck({ checks: [ { @@ -1603,7 +1565,6 @@ describe("OpenFGA Client", () => { ], }); - expect(scope.isDone()).toBe(true); expect(response.result).toHaveLength(2); expect(response.result[0].allowed).toBe(true); expect(response.result[1].allowed).toBe(false); @@ -1617,12 +1578,10 @@ describe("OpenFGA Client", () => { relation: "admin", object: "workspace:1", }; - const scope = nocks.expand(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); + nocks.expand(baseConfig.storeId!, tuple, undefined, ConsistencyPreference.HigherConsistency); - expect(scope.isDone()).toBe(false); const data = await fgaClient.expand(tuple, { authorizationModelId: "01GXSA8YR785C4FYS3C0RTG7B1", consistency: ConsistencyPreference.HigherConsistency }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -1630,9 +1589,8 @@ describe("OpenFGA Client", () => { describe("ListObjects", () => { it("should call the api and return the response", async () => { const mockedResponse = { objects: ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"] }; - const scope = nocks.listObjects(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); + nocks.listObjects(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); - expect(scope.isDone()).toBe(false); const response = await fgaClient.listObjects({ user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "can_read", @@ -1652,7 +1610,6 @@ describe("OpenFGA Client", () => { consistency: ConsistencyPreference.HigherConsistency, }); - expect(scope.isDone()).toBe(true); expect(response.objects).toHaveLength(mockedResponse.objects.length); expect(response.objects).toEqual(expect.arrayContaining(mockedResponse.objects)); }); @@ -1661,9 +1618,8 @@ describe("OpenFGA Client", () => { describe("StreamedListObjects", () => { it("should stream objects and yield them incrementally", async () => { const objects = ["document:1", "document:2", "document:3"]; - const scope = nocks.streamedListObjects(baseConfig.storeId!, objects); + nocks.streamedListObjects(baseConfig.storeId!, objects); - expect(scope.isDone()).toBe(false); const results: string[] = []; for await (const response of fgaClient.streamedListObjects({ @@ -1674,7 +1630,6 @@ describe("OpenFGA Client", () => { results.push(response.object); } - expect(scope.isDone()).toBe(true); expect(results).toHaveLength(3); expect(results).toEqual(expect.arrayContaining(objects)); }); @@ -1682,7 +1637,7 @@ describe("OpenFGA Client", () => { it("should handle custom headers", async () => { const objects = ["document:1"]; - const scope = nock(defaultConfiguration.getBasePath()) + nock(defaultConfiguration.getBasePath()) .post(`/stores/${baseConfig.storeId}/streamed-list-objects`) .reply(function () { // Verify custom headers were sent @@ -1713,12 +1668,11 @@ describe("OpenFGA Client", () => { results.push(response.object); } - expect(scope.isDone()).toBe(true); expect(results).toEqual(objects); }); it("should handle errors from the stream", async () => { - const scope = nock(defaultConfiguration.getBasePath()) + nock(defaultConfiguration.getBasePath()) .post(`/stores/${baseConfig.storeId}/streamed-list-objects`) .reply(500, { code: "internal_error", message: "Server error" }); @@ -1732,7 +1686,6 @@ describe("OpenFGA Client", () => { } }).rejects.toThrow(); - expect(scope.isDone()).toBe(true); }); it("should handle retry on 429 error", async () => { @@ -1746,7 +1699,7 @@ describe("OpenFGA Client", () => { }); // First attempt fails with 429 (called exactly once) - const scope1 = nock(defaultConfiguration.getBasePath()) + nock(defaultConfiguration.getBasePath()) .post(`/stores/${baseConfig.storeId}/streamed-list-objects`) .times(1) .reply(429, { code: "rate_limit_exceeded", message: "Rate limited" }, { @@ -1754,7 +1707,7 @@ describe("OpenFGA Client", () => { }); // Second attempt succeeds (retry - called exactly once) - const scope2 = nocks.streamedListObjects(baseConfig.storeId!, objects); + nocks.streamedListObjects(baseConfig.storeId!, objects); const results: string[] = []; for await (const response of fgaClientWithRetry.streamedListObjects({ @@ -1765,15 +1718,12 @@ describe("OpenFGA Client", () => { results.push(response.object); } - // Verify both scopes were called (proves retry happened) - expect(scope1.isDone()).toBe(true); - expect(scope2.isDone()).toBe(true); expect(results).toEqual(objects); }); it("should support consistency preference", async () => { const objects = ["document:1"]; - const scope = nocks.streamedListObjects(baseConfig.storeId!, objects); + nocks.streamedListObjects(baseConfig.storeId!, objects); const results: string[] = []; for await (const response of fgaClient.streamedListObjects({ @@ -1786,7 +1736,6 @@ describe("OpenFGA Client", () => { results.push(response.object); } - expect(scope.isDone()).toBe(true); expect(results).toEqual(objects); }); }); @@ -1846,6 +1795,9 @@ describe("OpenFGA Client", () => { expect(scope5.isDone()).toBe(false); expect(response.relations.length).toBe(2); expect(response.relations.sort()).toEqual(expect.arrayContaining(["admin", "reader"])); + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); it("should throw an error if any check returns an error", async () => { @@ -1908,6 +1860,9 @@ describe("OpenFGA Client", () => { expect(scope5.isDone()).toBe(false); expect(err).toBeInstanceOf(FgaApiError); } + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); it("should throw an error if no relations passed", async () => { @@ -1949,6 +1904,9 @@ describe("OpenFGA Client", () => { expect(scope0.isDone()).toBe(true); expect(scope1.isDone()).toBe(false); } + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. + nock.cleanAll(); }); }); @@ -1972,9 +1930,8 @@ describe("OpenFGA Client", () => { } }] }; - const scope = nocks.listUsers(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); + nocks.listUsers(baseConfig.storeId!, mockedResponse, undefined, ConsistencyPreference.HigherConsistency); - expect(scope.isDone()).toBe(false); const response = await fgaClient.listUsers({ object: { type: "document", @@ -2003,7 +1960,6 @@ describe("OpenFGA Client", () => { consistency: ConsistencyPreference.HigherConsistency }); - expect(scope.isDone()).toBe(true); expect(response.users).toHaveLength(mockedResponse.users.length); expect(response.users[0]).toMatchObject({ object: { @@ -2031,12 +1987,10 @@ describe("OpenFGA Client", () => { describe("ReadAssertions", () => { it("should properly call the ReadAssertions API", async () => { const modelId = "01H0THVNGCSAZ6SAQVTHPH3F0Q"; - const scope = nocks.readAssertions(defaultConfiguration.storeId!, modelId); + nocks.readAssertions(defaultConfiguration.storeId!, modelId); - expect(scope.isDone()).toBe(false); const data = await fgaClient.readAssertions({ authorizationModelId: modelId }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_model_id: modelId, assertions: expect.arrayContaining([]), @@ -2047,9 +2001,8 @@ describe("OpenFGA Client", () => { describe("WriteAssertions", () => { it("should properly call the WriteAssertions API", async () => { const modelId = "01H0THVNGCSAZ6SAQVTHPH3F0Q"; - const scope = nocks.writeAssertions(defaultConfiguration.storeId!, modelId); + nocks.writeAssertions(defaultConfiguration.storeId!, modelId); - expect(scope.isDone()).toBe(false); await fgaClient.writeAssertions([{ user: "user:81684243-9356-4421-8fbf-a4f8d36aa31b", relation: "viewer", @@ -2057,7 +2010,6 @@ describe("OpenFGA Client", () => { expectation: true, }], { authorizationModelId: modelId }); - expect(scope.isDone()).toBe(true); }); }); }); diff --git a/tests/credentials.test.ts b/tests/credentials.test.ts index fcc2bef..49b1884 100644 --- a/tests/credentials.test.ts +++ b/tests/credentials.test.ts @@ -16,21 +16,12 @@ describe("Credentials", () => { const mockTelemetryConfig: TelemetryConfiguration = new TelemetryConfiguration({}); describe("Refreshing access token", () => { - beforeEach(() => { - nock.disableNetConnect(); - }); - - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); - test("should use default scheme and token endpoint path when apiTokenIssuer has no scheme and no path", async () => { const apiTokenIssuer = "issuer.fga.example"; const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -52,8 +43,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should use default token endpoint path when apiTokenIssuer has root path and no scheme", async () => { @@ -61,7 +50,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -83,8 +72,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve custom token endpoint path when provided", async () => { @@ -92,7 +79,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = "/some_endpoint"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -114,8 +101,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve custom token endpoint path with nested path when provided", async () => { @@ -123,7 +108,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = "/api/v1/oauth/token"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -145,8 +130,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should add https:// prefix when apiTokenIssuer has no scheme", async () => { @@ -154,7 +137,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = "/some_endpoint"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -176,8 +159,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve http:// scheme when provided", async () => { @@ -185,7 +166,7 @@ describe("Credentials", () => { const expectedBaseUrl = "http://issuer.fga.example"; const expectedPath = "/some_endpoint"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -207,8 +188,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should use default path when apiTokenIssuer has https:// scheme but no path", async () => { @@ -216,7 +195,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -238,8 +217,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve custom path with query parameters", async () => { @@ -248,7 +225,7 @@ describe("Credentials", () => { const expectedPath = "/some_endpoint"; const queryParams = { param: "value" }; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .query(queryParams) .reply(200, { @@ -271,8 +248,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve custom path with port number", async () => { @@ -280,7 +255,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example:8080"; const expectedPath = "/some_endpoint"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -302,8 +277,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should use default path when path has multiple trailing slashes", async () => { @@ -311,7 +284,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -333,8 +306,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should use default path when path only consists of slashes", async () => { @@ -342,7 +313,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -364,8 +335,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should preserve custom path with consecutive/trailing slashes", async () => { @@ -373,7 +342,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = "/oauth//token///"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "test-token", @@ -395,8 +364,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test.each([ @@ -433,7 +400,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedAudience = "https://issuer.fga.example/some_endpoint/"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post("/some_endpoint", (body: string) => { const params = new URLSearchParams(body); const clientAssertion = params.get("client_assertion") as string; @@ -461,8 +428,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should normalize audience from apiTokenIssuer when using PrivateKeyJWT client credentials with HTTP scheme", async () => { @@ -470,7 +435,7 @@ describe("Credentials", () => { const expectedBaseUrl = "http://issuer.fga.example"; const expectedAudience = "http://issuer.fga.example/some_endpoint/"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post("/some_endpoint", (body: string) => { const params = new URLSearchParams(body); const clientAssertion = params.get("client_assertion") as string; @@ -498,8 +463,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should normalize audience from apiTokenIssuer when using PrivateKeyJWT client credentials with no scheme", async () => { @@ -507,7 +470,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedAudience = "https://issuer.fga.example/some_endpoint/"; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post("/some_endpoint", (body: string) => { const params = new URLSearchParams(body); const clientAssertion = params.get("client_assertion") as string; @@ -535,8 +498,6 @@ describe("Credentials", () => { ); await credentials.getAccessTokenHeader(); - - expect(scope.isDone()).toBe(true); }); test("should throw a real FgaApiAuthenticationError instance when token refresh fails", async () => { @@ -638,7 +599,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .once() .delay(20) @@ -668,7 +629,6 @@ describe("Credentials", () => { headers.forEach(header => { expect(header?.value).toBe("Bearer shared-token"); }); - expect(scope.isDone()).toBe(true); }); test("should clear shared refresh promise after failure and retry on the next call", async () => { @@ -676,7 +636,7 @@ describe("Credentials", () => { const expectedBaseUrl = "https://issuer.fga.example"; const expectedPath = `/${DEFAULT_TOKEN_ENDPOINT_PATH}`; - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .once() .reply(404, { @@ -718,7 +678,6 @@ describe("Credentials", () => { const header = await credentials.getAccessTokenHeader(); expect(header?.value).toBe("Bearer recovered-token"); - expect(scope.isDone()).toBe(true); }); test("should refresh cached token when it is close to expiration", async () => { @@ -731,7 +690,7 @@ describe("Credentials", () => { SdkConstants.TokenExpiryThresholdBufferInSec - 1 ); - const scope = nock(expectedBaseUrl) + nock(expectedBaseUrl) .post(expectedPath) .reply(200, { access_token: "short-lived-token", @@ -763,7 +722,6 @@ describe("Credentials", () => { expect(header1?.value).toBe("Bearer short-lived-token"); expect(header2?.value).toBe("Bearer refreshed-token"); - expect(scope.isDone()).toBe(true); } finally { randomSpy.mockRestore(); } diff --git a/tests/headers.test.ts b/tests/headers.test.ts index 95dae20..e1dd733 100644 --- a/tests/headers.test.ts +++ b/tests/headers.test.ts @@ -3,18 +3,12 @@ import { OpenFgaClient, UserClientConfigurationParams } from "../index"; import { baseConfig } from "./helpers/default-config"; import { CredentialsMethod } from "../credentials"; -nock.disableNetConnect(); - describe("Header Functionality Tests", () => { const testConfig: UserClientConfigurationParams = { ...baseConfig, credentials: { method: CredentialsMethod.None } }; - afterEach(() => { - nock.cleanAll(); - }); - describe("Default headers from client configuration", () => { it("should send default headers from baseOptions on all requests", async () => { const fgaClient = new OpenFgaClient({ @@ -28,7 +22,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function(this: nock.ReplyFnContext) { // Verify all default headers are present @@ -43,8 +37,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("should send default headers on multiple different API calls", async () => { @@ -58,7 +50,7 @@ describe("Header Functionality Tests", () => { }); // Test check endpoint - const checkScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { expect(this.req.headers["x-persistent-header"]).toBe("should-appear-everywhere"); @@ -66,7 +58,7 @@ describe("Header Functionality Tests", () => { }); // Test read endpoint - const readScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/read`) .reply(function() { expect(this.req.headers["x-persistent-header"]).toBe("should-appear-everywhere"); @@ -80,9 +72,6 @@ describe("Header Functionality Tests", () => { }); await fgaClient.read({}); - - expect(checkScope.isDone()).toBe(true); - expect(readScope.isDone()).toBe(true); }); }); @@ -90,7 +79,7 @@ describe("Header Functionality Tests", () => { it("should send per-request headers when specified", async () => { const fgaClient = new OpenFgaClient(testConfig); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { expect(this.req.headers["x-request-header"]).toBe("request-value"); @@ -108,15 +97,13 @@ describe("Header Functionality Tests", () => { "X-Correlation-ID": "abc-123-def" } }); - - expect(scope.isDone()).toBe(true); }); it("should only send per-request headers on the specific request", async () => { const fgaClient = new OpenFgaClient(testConfig); // First request with headers - const firstScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { expect(this.req.headers["x-first-request"]).toBe("first-value"); @@ -125,7 +112,7 @@ describe("Header Functionality Tests", () => { }); // Second request with different headers - const secondScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { expect(this.req.headers["x-second-request"]).toBe("second-value"); @@ -152,9 +139,6 @@ describe("Header Functionality Tests", () => { "X-Second-Request": "second-value" } }); - - expect(firstScope.isDone()).toBe(true); - expect(secondScope.isDone()).toBe(true); }); }); @@ -170,7 +154,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Verify default headers are present @@ -194,8 +178,6 @@ describe("Header Functionality Tests", () => { "X-User-Context": "test-user" } }); - - expect(scope.isDone()).toBe(true); }); it("should merge headers from multiple sources correctly", async () => { @@ -210,7 +192,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -241,8 +223,6 @@ describe("Header Functionality Tests", () => { "X-Timestamp": "2023-10-01" } }); - - expect(scope.isDone()).toBe(true); }); }); @@ -259,7 +239,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Per-request headers should override default headers @@ -281,8 +261,6 @@ describe("Header Functionality Tests", () => { "X-Shared-Header": "from-request" } }); - - expect(scope.isDone()).toBe(true); }); it("should preserve non-overridden default headers", async () => { @@ -297,7 +275,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Non-overridden defaults should remain @@ -319,8 +297,6 @@ describe("Header Functionality Tests", () => { "X-Override-This": "new-value" } }); - - expect(scope.isDone()).toBe(true); }); it("should handle case-insensitive header overrides correctly", async () => { @@ -333,7 +309,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // HTTP headers are case-insensitive, so request header should override default @@ -354,8 +330,6 @@ describe("Header Functionality Tests", () => { "x-test-header": "request-value" // Different case } }); - - expect(scope.isDone()).toBe(true); }); }); @@ -374,7 +348,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -393,8 +367,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("does not allow Content-Type override via per-request headers", async () => { @@ -402,7 +374,7 @@ describe("Header Functionality Tests", () => { const fgaClient = new OpenFgaClient(testConfig); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // SDK always enforces Content-Type for JSON APIs @@ -422,8 +394,6 @@ describe("Header Functionality Tests", () => { "X-Custom-Request": "request-value" // Custom headers still work } }); - - expect(scope.isDone()).toBe(true); }); it("should set Content-Type to application/json by default", async () => { @@ -439,7 +409,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -459,8 +429,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("SDK enforces Content-Type and Accept regardless of baseOptions headers", async () => { @@ -477,7 +445,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -497,8 +465,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); }); @@ -511,7 +477,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Should still have SDK headers @@ -526,8 +492,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("should handle undefined baseOptions", async () => { @@ -536,7 +500,7 @@ describe("Header Functionality Tests", () => { // No baseOptions specified }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Should still have SDK headers @@ -551,8 +515,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("should handle empty per-request headers", async () => { @@ -565,7 +527,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { // Default headers should still be present @@ -581,8 +543,6 @@ describe("Header Functionality Tests", () => { }, { headers: {} // Empty headers object }); - - expect(scope.isDone()).toBe(true); }); it("should handle special header values", async () => { @@ -598,7 +558,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -616,8 +576,6 @@ describe("Header Functionality Tests", () => { relation: "reader", object: "document:test" }); - - expect(scope.isDone()).toBe(true); }); it("should handle large number of headers", async () => { @@ -641,7 +599,7 @@ describe("Header Functionality Tests", () => { } }); - const scope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { const headers = this.req.headers; @@ -666,8 +624,6 @@ describe("Header Functionality Tests", () => { }, { headers: requestHeaders }); - - expect(scope.isDone()).toBe(true); }); }); @@ -683,21 +639,21 @@ describe("Header Functionality Tests", () => { }); // Test multiple endpoints - const checkScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/check`) .reply(function() { expect(this.req.headers["x-consistent-header"]).toBe("always-present"); return [200, { allowed: true }]; }); - const readScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/read`) .reply(function() { expect(this.req.headers["x-consistent-header"]).toBe("always-present"); return [200, { tuples: [] }]; }); - const writeScope = nock(testConfig.apiUrl!) + nock(testConfig.apiUrl!) .post(`/stores/${testConfig.storeId}/write`) .reply(function() { expect(this.req.headers["x-consistent-header"]).toBe("always-present"); @@ -719,10 +675,6 @@ describe("Header Functionality Tests", () => { object: "document:test" }] }); - - expect(checkScope.isDone()).toBe(true); - expect(readScope.isDone()).toBe(true); - expect(writeScope.isDone()).toBe(true); }); }); }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 9e8b147..2c542c6 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -27,7 +27,6 @@ import { import { getNocks } from "./helpers/nocks"; const nocks = getNocks(nock); -nock.disableNetConnect(); describe("OpenFGA SDK", function () { describe("initializing the sdk", () => { @@ -197,17 +196,10 @@ describe("OpenFGA SDK", function () { }); it("should issue a network call to get the token at the first request if client id is provided", async () => { - const scope = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); + nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); nocks.readAuthorizationModels(baseConfig.storeId!); - const fgaApi = new OpenFgaApi(baseConfig); - expect(scope.isDone()).toBe(false); - await fgaApi.readAuthorizationModels(baseConfig.storeId!); - - expect(scope.isDone()).toBe(true); - - nock.cleanAll(); }); it("should cache the bearer token and not issue a network call to get the token at the second request", async () => { @@ -223,7 +215,6 @@ describe("OpenFGA SDK", function () { expect(scope.isDone()).toBe(true); - nock.cleanAll(); scope = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); nocks.readAuthorizationModels(baseConfig.storeId!); expect(scope.isDone()).toBe(false); @@ -231,27 +222,19 @@ describe("OpenFGA SDK", function () { await fgaApi.readAuthorizationModels(baseConfig.storeId!); expect(scope.isDone()).toBe(false); - + + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. nock.cleanAll(); }); it("should retry a failed attempt to request to exchange the credentials", async () => { - const scope1 = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token", 300, 500, { + nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token", 300, 500, { "Retry-After": "1" // Add Retry-After header }); - const scope2 = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); + nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); nocks.readAuthorizationModels(baseConfig.storeId!); - const fgaApi = new OpenFgaApi(baseConfig); - expect(scope1.isDone()).toBe(false); - expect(scope2.isDone()).toBe(false); - await fgaApi.readAuthorizationModels(baseConfig.storeId!); - - expect(scope1.isDone()).toBe(true); - expect(scope2.isDone()).toBe(true); - - nock.cleanAll(); }); it("should not issue a network call to get the token at the first request if the clientId is not provided", async () => { @@ -267,13 +250,13 @@ describe("OpenFGA SDK", function () { expect(scope.isDone()).toBe(false); + // NOTE: manually clean pending mocks as we assert on _not_ being matched above. nock.cleanAll(); }); it("should issue a network call to get the token at the first request if client assertion is provided", async () => { - const scope = nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); + nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); nocks.readAuthorizationModels(baseConfig.storeId!); - const fgaApi = new OpenFgaApi({ ...baseConfig, credentials: { @@ -284,13 +267,7 @@ describe("OpenFGA SDK", function () { } } }); - expect(scope.isDone()).toBe(false); - await fgaApi.readAuthorizationModels(baseConfig.storeId!); - - expect(scope.isDone()).toBe(true); - - nock.cleanAll(); }); it("should allow passing in a configuration instance", async () => { @@ -359,10 +336,6 @@ describe("OpenFGA SDK", function () { }); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should throw FgaApiValidationError", async () => { await expect( fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey }) @@ -414,10 +387,6 @@ describe("OpenFGA SDK", function () { }); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should throw FgaApiRateLimitExceededError", async () => { await expect( fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey }, {}) @@ -431,14 +400,13 @@ describe("OpenFGA SDK", function () { relation: "viewer", object: "foobar:x", }; - let failedRequestNock: nock.Scope; beforeEach(async () => { fgaApi = new OpenFgaApi({ ...baseConfig }); nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); - failedRequestNock = nock(basePath) + nock(basePath) .post( `/stores/${storeId}/check`, { @@ -456,14 +424,9 @@ describe("OpenFGA SDK", function () { nocks.check(baseConfig.storeId!, tupleKey); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should return allowed", async () => { const result = await fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey, authorization_model_id: "01GXSA8YR785C4FYS3C0RTG7B1"}, {}); - expect(failedRequestNock.isDone()).toBe(true); expect(result.allowed).toBe(true); }); }); @@ -474,7 +437,6 @@ describe("OpenFGA SDK", function () { relation: "viewer", object: "foobar:x", }; - let failedRequestNock: nock.Scope; beforeEach(async () => { const updateBaseConfig = { @@ -485,7 +447,7 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); - failedRequestNock = nock(basePath) + nock(basePath) .post( `/stores/${storeId}/check`, { @@ -503,14 +465,9 @@ describe("OpenFGA SDK", function () { nocks.check(baseConfig.storeId!, tupleKey); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should return allowed", async () => { const result = await fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey, authorization_model_id: "01GXSA8YR785C4FYS3C0RTG7B1"}, {}); - expect(failedRequestNock.isDone()).toBe(true); expect(result.allowed).toBe(true); }); }); @@ -521,12 +478,11 @@ describe("OpenFGA SDK", function () { relation: "viewer", object: "foobar:x", }; - let failedRequestNock: nock.Scope; beforeEach(async () => { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); - failedRequestNock = nock(basePath) + nock(basePath) .post( `/stores/${storeId}/check`, { @@ -543,10 +499,6 @@ describe("OpenFGA SDK", function () { nocks.check(baseConfig.storeId!, tupleKey); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should return allowed", async () => { const result = await fgaApi.check( baseConfig.storeId!, @@ -554,7 +506,6 @@ describe("OpenFGA SDK", function () { { retryParams: GetDefaultRetryParams(2, 10) } ); - expect(failedRequestNock.isDone()).toBe(true); expect(result.allowed).toBe(true); }); }); @@ -570,10 +521,6 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should throw FgaApiInternalError if retries disabled", async () => { nock(basePath) .post( @@ -684,10 +631,6 @@ describe("OpenFGA SDK", function () { }); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should throw FgaApiNotFoundError", async () => { await expect( fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey }) @@ -706,23 +649,6 @@ describe("OpenFGA SDK", function () { nock(`https://${ OPENFGA_API_TOKEN_ISSUER}`) .post("/oauth/token") .reply(401); - - nock(basePath) - .post( - `/stores/${storeId}/check`, - { - tuple_key: tupleKey, - }, - expect.objectContaining({ Authorization: "Bearer test-token" }) - ) - .reply(500, { - code: "invalid_claims", - message: "nock error", - }); - }); - - afterEach(() => { - nock.cleanAll(); }); it("should throw FgaApiAuthenticationError", async () => { @@ -732,6 +658,7 @@ describe("OpenFGA SDK", function () { ).rejects.toThrow(FgaApiAuthenticationError); }); }); + describe("non-Axios errors should be thrown immediately without retry", () => { it("should throw FgaError immediately for non-Axios errors", async () => { const tupleKey = { @@ -784,10 +711,6 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should retry once (2 total attempts) when maxRetry=1 for 500 error", async () => { // First attempt fails with 500 nock(basePath) @@ -887,10 +810,6 @@ describe("OpenFGA SDK", function () { fgaApi = new OpenFgaApi({ ...baseConfig }); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should handle non-401 errors during token exchange", async () => { nock(`https://${OPENFGA_API_TOKEN_ISSUER}`) .post("/oauth/token") @@ -933,10 +852,6 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should use exponential backoff when Retry-After header is missing", async () => { // First request fails with 429, no Retry-After header nock(basePath) @@ -1235,10 +1150,6 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER, "test-token"); }); - afterEach(() => { - nock.cleanAll(); - }); - it("should not retry 501 Not Implemented errors", async () => { // Mock a single 501 error - should not be retried nock(basePath) @@ -1329,13 +1240,8 @@ describe("OpenFGA SDK", function () { relation: "abc", object: "foobar:x", }; - const scope = nocks.check(baseConfig.storeId!, tupleKey); - expect(scope.isDone()).toBe(false); - + nocks.check(baseConfig.storeId!, tupleKey); result = await fgaApi.check(baseConfig.storeId!, { tuple_key: tupleKey }); - expect(scope.isDone()).toBe(true); - - nock.cleanAll(); }); it("should return allowed", () => { @@ -1362,10 +1268,6 @@ describe("OpenFGA SDK", function () { nocks.tokenExchange(OPENFGA_API_TOKEN_ISSUER); }); - afterEach(() => { - nock.cleanAll(); - }); - describe("check", () => { it("should properly pass the request and return an allowed API response", async () => { const tuple = { @@ -1373,12 +1275,8 @@ describe("OpenFGA SDK", function () { relation: "admin", object: "workspace:1", }; - const scope = nocks.check(baseConfig.storeId!, tuple); - - expect(scope.isDone()).toBe(false); + nocks.check(baseConfig.storeId!, tuple); const data = await fgaApi.check(baseConfig.storeId!, { tuple_key: tuple }); - - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ allowed: expect.any(Boolean) }); }); }); @@ -1390,9 +1288,8 @@ describe("OpenFGA SDK", function () { relation: "admin", object: "workspace:1", }; - const scope = nocks.write(baseConfig.storeId!); + nocks.write(baseConfig.storeId!); - expect(scope.isDone()).toBe(false); const data = await fgaApi.write( baseConfig.storeId!, { @@ -1400,7 +1297,6 @@ describe("OpenFGA SDK", function () { authorization_model_id: "01GXSA8YR785C4FYS3C0RTG7B1", }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -1412,17 +1308,14 @@ describe("OpenFGA SDK", function () { relation: "admin", object: "workspace:1", }; - const scope = nocks.delete(baseConfig.storeId!, tuple); + nocks.delete(baseConfig.storeId!, tuple); - expect(scope.isDone()).toBe(false); const data = await fgaApi.write( baseConfig.storeId!, { deletes: { tuple_keys: [tuple] }, authorization_model_id: "01GXSA8YR785C4FYS3C0RTG7B1", }); - - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -1434,14 +1327,12 @@ describe("OpenFGA SDK", function () { relation: "admin", object: "workspace:1", }; - const scope = nocks.expand(baseConfig.storeId!, tuple); + nocks.expand(baseConfig.storeId!, tuple); - expect(scope.isDone()).toBe(false); const data = await fgaApi.expand( baseConfig.storeId!, { tuple_key: tuple, authorization_model_id: "01GXSA8YR785C4FYS3C0RTG7B1"}); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -1453,12 +1344,10 @@ describe("OpenFGA SDK", function () { relation: "admin", object: "workspace:1", }; - const scope = nocks.read(baseConfig.storeId!, tuple); + nocks.read(baseConfig.storeId!, tuple); - expect(scope.isDone()).toBe(false); const data = await fgaApi.read(baseConfig.storeId!, { tuple_key: tuple }); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({}); }); }); @@ -1471,18 +1360,16 @@ describe("OpenFGA SDK", function () { { type: "workspace", relations: { admin: { this: {} } } }, ], }; - const scope = nocks.writeAuthorizationModel( + nocks.writeAuthorizationModel( baseConfig.storeId!, authorizationModel ); - expect(scope.isDone()).toBe(false); const data = await fgaApi.writeAuthorizationModel( baseConfig.storeId!, authorizationModel ); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ id: expect.any(String) }); }); }); @@ -1490,12 +1377,10 @@ describe("OpenFGA SDK", function () { describe("readAuthorizationModel", () => { it("should call the api and return the response", async () => { const configId = "string"; - const scope = nocks.readSingleAuthzModel(baseConfig.storeId!, configId); + nocks.readSingleAuthzModel(baseConfig.storeId!, configId); - expect(scope.isDone()).toBe(false); const data = await fgaApi.readAuthorizationModel(baseConfig.storeId!, configId); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_model: { id: expect.any(String), @@ -1508,12 +1393,10 @@ describe("OpenFGA SDK", function () { describe("readAuthorizationModels", () => { it("should call the api and return the response", async () => { - const scope = nocks.readAuthorizationModels(baseConfig.storeId!, defaultConfiguration.getBasePath(), [{ id: "1", schema_version: "1.1", type_definitions: []}]); + nocks.readAuthorizationModels(baseConfig.storeId!, defaultConfiguration.getBasePath(), [{ id: "1", schema_version: "1.1", type_definitions: []}]); - expect(scope.isDone()).toBe(false); const data = await fgaApi.readAuthorizationModels(baseConfig.storeId!); - expect(scope.isDone()).toBe(true); expect(data).toMatchObject({ authorization_models: expect.arrayContaining([{ id: "1", schema_version: "1.1", type_definitions: []}]), }); @@ -1527,12 +1410,10 @@ describe("OpenFGA SDK", function () { const continuationToken = "eyJwayI6IkxBVEVTVF9OU0NPTkZJR19hdXRoMHN0b3JlIiwic2siOiIxem1qbXF3MWZLZExTcUoyN01MdTdqTjh0cWgifQ=="; const startTime = "2022-01-01T00:00:00Z"; - const scope = nocks.readChanges(baseConfig.storeId!, type, pageSize, continuationToken, startTime); + nocks.readChanges(baseConfig.storeId!, type, pageSize, continuationToken, startTime); - expect(scope.isDone()).toBe(false); const response = await fgaApi.readChanges(baseConfig.storeId!, type, pageSize, continuationToken, startTime); - expect(scope.isDone()).toBe(true); expect(response).toMatchObject({ changes: expect.arrayContaining([]) }); }); }); @@ -1540,9 +1421,8 @@ describe("OpenFGA SDK", function () { describe("listObjects", () => { it("should call the api and return the response", async () => { const mockedResponse = { objects: ["document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"] }; - const scope = nocks.listObjects(baseConfig.storeId!, mockedResponse); + nocks.listObjects(baseConfig.storeId!, mockedResponse); - expect(scope.isDone()).toBe(false); const response = await fgaApi.listObjects( baseConfig.storeId!, { @@ -1564,7 +1444,6 @@ describe("OpenFGA SDK", function () { } }); - expect(scope.isDone()).toBe(true); expect(response.objects).toHaveLength(mockedResponse.objects.length); expect(response.objects).toEqual(expect.arrayContaining(mockedResponse.objects)); }); diff --git a/tests/setup.ts b/tests/setup.ts index bd282d0..a70df64 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,9 +1,19 @@ import nock from "nock"; +// NOTE: Do not replace host constant with `OPENFGA_API_URL` import from `helpers/default-config`, +// as this very file imports `ClientConfiguration` class, and this in turn will invalidate +// jest's mock hoisting and break all telemetry tests. +const OPENFGA_API_URL_HOST = "api.fga.example"; + beforeEach(() => { nock.disableNetConnect(); + nock.enableNetConnect((host) => host.startsWith(OPENFGA_API_URL_HOST)); }); -afterAll(() => { - nock.restore(); -}); \ No newline at end of file +afterEach(() => { + const pendingMocks = nock.pendingMocks(); + nock.cleanAll(); + if (pendingMocks.length > 0) { + throw new Error(`Pending Nock mocks found: ${pendingMocks.join(",")}`); + } +});