From 603add59d179136b1de132328ec29935c1288bf1 Mon Sep 17 00:00:00 2001 From: gauravmann Date: Wed, 5 Nov 2025 16:22:34 +0530 Subject: [PATCH] testing auth types --- .../akto/utils/CustomAuthTestDataMother.java | 339 ++++++++++++++ .../com/akto/utils/CustomAuthUtilTest.java | 426 ++++++++++++++++++ 2 files changed, 765 insertions(+) create mode 100644 libs/utils/src/test/java/com/akto/utils/CustomAuthTestDataMother.java create mode 100644 libs/utils/src/test/java/com/akto/utils/CustomAuthUtilTest.java diff --git a/libs/utils/src/test/java/com/akto/utils/CustomAuthTestDataMother.java b/libs/utils/src/test/java/com/akto/utils/CustomAuthTestDataMother.java new file mode 100644 index 0000000000..3ff6fa2933 --- /dev/null +++ b/libs/utils/src/test/java/com/akto/utils/CustomAuthTestDataMother.java @@ -0,0 +1,339 @@ +package com.akto.utils; + +import com.akto.dto.ApiInfo; +import com.akto.dto.traffic.Key; +import com.akto.dto.traffic.SampleData; +import com.akto.dto.type.URLMethods; + +import java.util.*; + +/** + * Object Mother pattern for creating test data for CustomAuthUtil tests. + * Provides pre-configured test objects representing common test scenarios. + */ +public class CustomAuthTestDataMother { + + public static final int TEST_API_COLLECTION_ID_1 = 1111111111; + public static final int TEST_API_COLLECTION_ID_2 = 22222222; + public static final int TEST_API_COLLECTION_ID_3 = 3333333; + public static final int TEST_API_COLLECTION_ID_4 = 44444444; + + // ==================== ApiInfo Creation ==================== + + /** + * Creates ApiInfo with no existing auth types (empty set). + * URL: /api/users, Method: GET + */ + public static ApiInfo createApiInfoWithNoAuth() { + return createApiInfo( + TEST_API_COLLECTION_ID_1, + "https://vulnerable-server.akto.io/api/users", + URLMethods.Method.GET, + new HashSet<>() + ); + } + + /** + * Creates ApiInfo with existing JWT and AUTHORIZATION_HEADER auth types. + * URL: /api/college/eco/students, Method: POST + */ + public static ApiInfo createApiInfoWithSingleAuthSet() { + return createApiInfo( + TEST_API_COLLECTION_ID_2, + "https://vulnerable-server.akto.io/api/college/eco/students", + URLMethods.Method.POST, + createAuthTypeSets( + new ApiInfo.AuthType[]{ApiInfo.AuthType.JWT, ApiInfo.AuthType.AUTHORIZATION_HEADER} + ) + ); + } + + /** + * Creates ApiInfo with multiple auth type sets: [[JWT], [BASIC, API_KEY]]. + * URL: /api/products, Method: PUT + */ + public static ApiInfo createApiInfoWithMultipleAuthSets() { + return createApiInfo( + TEST_API_COLLECTION_ID_3, + "https://vulnerable-server.akto.io/api/products", + URLMethods.Method.PUT, + createAuthTypeSets( + new ApiInfo.AuthType[]{ApiInfo.AuthType.JWT}, + new ApiInfo.AuthType[]{ApiInfo.AuthType.BASIC, ApiInfo.AuthType.API_KEY} + ) + ); + } + + /** + * Creates ApiInfo with multiple auth type sets for testing preservation and enhancement. + * Initial state: [[JWT], [BASIC, AUTHORIZATION_HEADER]] + * URL: /api/test/preserve-enhance, Method: POST + */ + public static ApiInfo createApiInfoForPreserveAndEnhance() { + return createApiInfo( + TEST_API_COLLECTION_ID_4, + "https://vulnerable-server.akto.io/api/test/preserve-enhance", + URLMethods.Method.POST, + createAuthTypeSets( + new ApiInfo.AuthType[]{ApiInfo.AuthType.JWT}, + new ApiInfo.AuthType[]{ApiInfo.AuthType.BASIC, ApiInfo.AuthType.AUTHORIZATION_HEADER} + ) + ); + } + + + // ==================== SampleData Creation ==================== + + + + /** + * Creates SampleData for API 2 with JWT prefix token (2 samples). + */ + public static SampleData createSampleDataWithJwtPrefix() { + String sample1 = createSampleJson( + TEST_API_COLLECTION_ID_2, + "https://vulnerable-server.akto.io/api/college/eco/students", + "POST", + "{\\\"authorization\\\":\\\"JWT eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJBa3RvIiwic3ViIjoibG9naW4ifQ.ToSrgQdEWaTVBphY9QMPBmo1zWga\\\",\\\"content-length\\\":\\\"2\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"User Access Denied\\\"}" + ); + + String sample2 = createSampleJson( + TEST_API_COLLECTION_ID_2, + "https://vulnerable-server.akto.io/api/college/eco/students", + "POST", + "{\\\"authorization\\\":\\\"JWT eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJBa3RvIiwic3ViIjoibG9naW4ifQ.ToSrgQdEWaTVBphY9QMPBmo1zWga\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData( + TEST_API_COLLECTION_ID_2, + "https://vulnerable-server.akto.io/api/college/eco/students", + URLMethods.Method.POST, + Arrays.asList(sample1, sample2) + ); + } + + /** + * Creates SampleData for API 3 with Basic auth and API key. + */ + public static SampleData createSampleDataWithBasicAndApiKey() { + String sample = createSampleJson( + TEST_API_COLLECTION_ID_3, + "https://vulnerable-server.akto.io/api/products", + "PUT", + "{\\\"authorization\\\":\\\"Basic dXNlcjpwYXNzd29yZA==\\\",\\\"x-api-key\\\":\\\"abc123xyz\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{\\\"name\\\":\\\"Product1\\\"}", + "{\\\"id\\\":123}" + ); + + return createSampleData( + TEST_API_COLLECTION_ID_3, + "https://vulnerable-server.akto.io/api/products", + URLMethods.Method.PUT, + Arrays.asList(sample) + ); + } + + /** + * Creates SampleData for API 1 with Bearer JWT token in authorization header. + */ + public static SampleData createSampleDataWithBearerToken(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"authorization\\\":\\\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"users\\\":[{\\\"id\\\":1,\\\"name\\\":\\\"John\\\"}]}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with no authentication headers. + */ + public static SampleData createSampleDataWithNoAuth(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with API key header. + */ + public static SampleData createSampleDataWithApiKey(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"x-api-key\\\":\\\"abc123xyz456\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with Basic authentication. + */ + public static SampleData createSampleDataWithBasicAuth(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"authorization\\\":\\\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with MTLS (client certificate). + */ + public static SampleData createSampleDataWithMtls(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"clientcert\\\":\\\"MIIDXTCCAkWgAwIBAgIJAKL0UG4+...\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with session token. + */ + public static SampleData createSampleDataWithSessionToken(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"session-token\\\":\\\"sess_abc123xyz456789\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with multiple auth types (Authorization + MTLS). + */ + public static SampleData createSampleDataWithAuthorizationAndMtls(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"authorization\\\":\\\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U\\\",\\\"clientcert\\\":\\\"MIIDXTCCAkWgAwIBAgIJAKL0UG4+...\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with Basic auth + API key. + */ + public static SampleData createSampleDataWithBasicAndApiKey(int apiCollectionId, String url, URLMethods.Method method) { + String sample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"authorization\\\":\\\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\\\",\\\"x-api-key\\\":\\\"abc123xyz\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + + return createSampleData(apiCollectionId, url, method, Arrays.asList(sample)); + } + + /** + * Creates SampleData with TWO samples: one with API_KEY, one with MTLS. + * This is for testing when multiple different auth types come from different samples. + */ + public static SampleData createSampleDataWithApiKeyAndMtlsSamples(int apiCollectionId, String url, URLMethods.Method method) { + List samples = new ArrayList<>(); + + // Sample 1: API_KEY + String apiKeySample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"x-api-key\\\":\\\"abc123xyz456\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + samples.add(apiKeySample); + + // Sample 2: MTLS + String mtlsSample = createSampleJson( + apiCollectionId, + url, + method.name(), + "{\\\"clientcert\\\":\\\"MIIDXTCCAkWgAwIBAgIJAKL0UG4+...\\\",\\\"content-type\\\":\\\"application/json\\\"}", + "{}", + "{\\\"message\\\":\\\"Success\\\"}" + ); + samples.add(mtlsSample); + + return createSampleData(apiCollectionId, url, method, samples); + } + + + // ==================== Helper Methods ==================== + + private static ApiInfo createApiInfo(int apiCollectionId, String url, URLMethods.Method method, + Set> allAuthTypesFound) { + ApiInfo apiInfo = new ApiInfo(apiCollectionId, url, method); + apiInfo.setAllAuthTypesFound(allAuthTypesFound); + apiInfo.setLastSeen(1758562434); + apiInfo.setCollectionIds(Arrays.asList(apiCollectionId)); + return apiInfo; + } + + private static SampleData createSampleData(int apiCollectionId, String url, URLMethods.Method method, + List samples) { + Key key = new Key(apiCollectionId, url, method, -1, 0, 0); + SampleData sampleData = new SampleData(key, samples); + sampleData.setCollectionIds(Arrays.asList(apiCollectionId)); + return sampleData; + } + + private static String createSampleJson(int apiCollectionId, String url, String method, + String requestHeaders, String requestPayload, String responsePayload) { + return String.format( + "{\"destIp\":null,\"method\":\"%s\",\"requestPayload\":\"%s\",\"responsePayload\":\"%s\"," + + "\"ip\":\"null\",\"source\":\"HAR\",\"type\":\"HTTP/1.1\",\"akto_vxlan_id\":%d," + + "\"path\":\"%s\",\"requestHeaders\":\"%s\",\"responseHeaders\":\"{}\"," + + "\"time\":\"1758219946\",\"statusCode\":\"200\",\"status\":\"OK\"," + + "\"akto_account_id\":\"1758179941\",\"is_pending\":\"false\"}", + method, requestPayload, responsePayload, apiCollectionId, url, requestHeaders + ); + } + + private static Set> createAuthTypeSets(ApiInfo.AuthType[]... authTypeSets) { + Set> result = new HashSet<>(); + for (ApiInfo.AuthType[] authTypes : authTypeSets) { + result.add(new HashSet<>(Arrays.asList(authTypes))); + } + return result; + } +} diff --git a/libs/utils/src/test/java/com/akto/utils/CustomAuthUtilTest.java b/libs/utils/src/test/java/com/akto/utils/CustomAuthUtilTest.java new file mode 100644 index 0000000000..cfe4993274 --- /dev/null +++ b/libs/utils/src/test/java/com/akto/utils/CustomAuthUtilTest.java @@ -0,0 +1,426 @@ +package com.akto.utils; + +import com.akto.MongoBasedTest; +import com.akto.dao.ApiInfoDao; +import com.akto.dao.SampleDataDao; +import com.akto.dto.ApiInfo; +import com.mongodb.client.model.WriteModel; +import org.junit.After; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.akto.utils.CustomAuthTestDataMother.*; +import static org.junit.Assert.*; + +public class CustomAuthUtilTest extends MongoBasedTest { + + @After + public void tearDown() { + // Explicitly stop MongoDB after each test to prevent port conflicts + if (mongod != null) { + mongod.stop(); + } + if (mongodExe != null) { + mongodExe.stop(); + } + } + + /** + * Test Group 1: Single Auth Type Detection + * Tests detection of single auth types from scratch (empty initial state) + */ + @Test + public void testCalcAuthWithSingleAuthTypes() { + // Clear collections + ApiInfoDao.instance.getMCollection().drop(); + SampleDataDao.instance.getMCollection().drop(); + + // Setup: Create 5 APIs with empty auth types and different sample data + List apiInfoList = new ArrayList<>(); + + // API 1: Bearer JWT + ApiInfo api1 = createApiInfoWithNoAuth(); + api1.getId().setUrl("/api/test/bearer"); + apiInfoList.add(api1); + SampleDataDao.instance.insertOne(createSampleDataWithBearerToken( + api1.getId().getApiCollectionId(), + api1.getId().getUrl(), + api1.getId().getMethod() + )); + + // API 2: API Key + ApiInfo api2 = createApiInfoWithNoAuth(); + api2.getId().setUrl("/api/test/apikey"); + apiInfoList.add(api2); + SampleDataDao.instance.insertOne(createSampleDataWithApiKey( + api2.getId().getApiCollectionId(), + api2.getId().getUrl(), + api2.getId().getMethod() + )); + + // API 3: Basic Auth + ApiInfo api3 = createApiInfoWithNoAuth(); + api3.getId().setUrl("/api/test/basic"); + apiInfoList.add(api3); + SampleDataDao.instance.insertOne(createSampleDataWithBasicAuth( + api3.getId().getApiCollectionId(), + api3.getId().getUrl(), + api3.getId().getMethod() + )); + + // API 4: MTLS + ApiInfo api4 = createApiInfoWithNoAuth(); + api4.getId().setUrl("/api/test/mtls"); + apiInfoList.add(api4); + SampleDataDao.instance.insertOne(createSampleDataWithMtls( + api4.getId().getApiCollectionId(), + api4.getId().getUrl(), + api4.getId().getMethod() + )); + + // API 5: Session Token + ApiInfo api5 = createApiInfoWithNoAuth(); + api5.getId().setUrl("/api/test/session"); + apiInfoList.add(api5); + SampleDataDao.instance.insertOne(createSampleDataWithSessionToken( + api5.getId().getApiCollectionId(), + api5.getId().getUrl(), + api5.getId().getMethod() + )); + + ApiInfoDao.instance.insertMany(apiInfoList); + + // Execute: Call calcAuth + List> updates = CustomAuthUtil.calcAuth(apiInfoList, null, true); + + // Assert in-memory state: Should have 5 updates (one per API) + assertEquals("Should have 5 WriteModel updates", 5, updates.size()); + + // Verify each API has auth types detected + for (ApiInfo apiInfo : apiInfoList) { + assertNotNull("Auth types should not be null", apiInfo.getAllAuthTypesFound()); + assertFalse("Auth types should not be empty", apiInfo.getAllAuthTypesFound().isEmpty()); + } + + // Execute bulk write to persist changes + if (!updates.isEmpty()) { + ApiInfoDao.instance.getMCollection().bulkWrite(updates); + } + + // Assert DB state: Verify persisted auth types + for (ApiInfo apiInfo : apiInfoList) { + ApiInfo dbApiInfo = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(apiInfo.getId())); + assertNotNull("DB ApiInfo should exist", dbApiInfo); + assertNotNull("DB auth types should not be null", dbApiInfo.getAllAuthTypesFound()); + assertEquals("In-memory and DB auth types should match", + apiInfo.getAllAuthTypesFound(), + dbApiInfo.getAllAuthTypesFound()); + } + } + + /** + * Test Group 2: Multiple Auth Types in Single Request + * Tests detection of multiple auth types present in the same sample + */ + @Test + public void testCalcAuthWithMultipleAuthTypes() { + // Clear collections + ApiInfoDao.instance.getMCollection().drop(); + SampleDataDao.instance.getMCollection().drop(); + + // Setup: Create 2 APIs with multiple auth types in samples + List apiInfoList = new ArrayList<>(); + + // API 1: Authorization + MTLS + ApiInfo api1 = createApiInfoWithNoAuth(); + api1.getId().setUrl("/api/test/auth-mtls"); + apiInfoList.add(api1); + SampleDataDao.instance.insertOne(createSampleDataWithAuthorizationAndMtls( + api1.getId().getApiCollectionId(), + api1.getId().getUrl(), + api1.getId().getMethod() + )); + + // API 2: Basic + API Key + ApiInfo api2 = createApiInfoWithNoAuth(); + api2.getId().setUrl("/api/test/basic-apikey"); + apiInfoList.add(api2); + SampleDataDao.instance.insertOne(createSampleDataWithBasicAndApiKey( + api2.getId().getApiCollectionId(), + api2.getId().getUrl(), + api2.getId().getMethod() + )); + + ApiInfoDao.instance.insertMany(apiInfoList); + + // Execute: Call calcAuth + List> updates = CustomAuthUtil.calcAuth(apiInfoList, null, true); + + // Assert in-memory state: Should have 2 updates + assertEquals("Should have 2 WriteModel updates", 2, updates.size()); + + // Verify multiple auth types detected in each API + for (ApiInfo apiInfo : apiInfoList) { + Set> authTypes = apiInfo.getAllAuthTypesFound(); + assertNotNull("Auth types should not be null", authTypes); + + // Each API should have at least one set of auth types + assertFalse("Auth types should not be empty", authTypes.isEmpty()); + + // Verify that at least one set has multiple auth types (if they're grouped together) + // or multiple individual auth types are detected + int totalAuthTypeCount = authTypes.stream() + .mapToInt(Set::size) + .sum(); + assertTrue("Should detect multiple auth types", totalAuthTypeCount >= 2); + } + + // Execute bulk write + if (!updates.isEmpty()) { + ApiInfoDao.instance.getMCollection().bulkWrite(updates); + } + + // Assert DB state + for (ApiInfo apiInfo : apiInfoList) { + ApiInfo dbApiInfo = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(apiInfo.getId())); + assertNotNull("DB ApiInfo should exist", dbApiInfo); + assertEquals("In-memory and DB auth types should match", + apiInfo.getAllAuthTypesFound(), + dbApiInfo.getAllAuthTypesFound()); + } + } + + /** + * Test Group 3: Existing Auth Types Preserved/Updated + * Tests that existing auth types are maintained or enhanced + */ + @Test + public void testCalcAuthWithExistingAuthTypes() { + // Clear collections + ApiInfoDao.instance.getMCollection().drop(); + SampleDataDao.instance.getMCollection().drop(); + + // Setup: Create APIs with existing auth types + List apiInfoList = new ArrayList<>(); + + // API 1: Already has JWT+AUTHORIZATION_HEADER, should maintain + ApiInfo api1 = createApiInfoWithSingleAuthSet(); + apiInfoList.add(api1); + SampleDataDao.instance.insertOne(createSampleDataWithJwtPrefix()); + + // API 2: Already has multiple auth sets, should maintain/update + ApiInfo api2 = createApiInfoWithMultipleAuthSets(); + apiInfoList.add(api2); + SampleDataDao.instance.insertOne(createSampleDataWithBasicAndApiKey()); + + ApiInfoDao.instance.insertMany(apiInfoList); + + // Store initial auth types for comparison + Set> api1InitialAuth = new HashSet<>(api1.getAllAuthTypesFound()); + Set> api2InitialAuth = new HashSet<>(api2.getAllAuthTypesFound()); + + // Execute: Call calcAuth + List> updates = CustomAuthUtil.calcAuth(apiInfoList, null, true); + + // Assert in-memory state: Should have 2 updates + assertEquals("Should have 2 WriteModel updates", 2, updates.size()); + + // Verify auth types are not empty (either preserved or enhanced) + assertNotNull("API1 auth types should not be null", api1.getAllAuthTypesFound()); + assertFalse("API1 auth types should not be empty", api1.getAllAuthTypesFound().isEmpty()); + + assertNotNull("API2 auth types should not be null", api2.getAllAuthTypesFound()); + assertFalse("API2 auth types should not be empty", api2.getAllAuthTypesFound().isEmpty()); + + // Verify existing auth types are preserved + assertTrue("API1 should preserve all initial auth types", + api1.getAllAuthTypesFound().containsAll(api1InitialAuth)); + + assertTrue("API2 should preserve all initial auth types", + api2.getAllAuthTypesFound().containsAll(api2InitialAuth)); + + // Verify auth types can be enhanced (size >= initial size) + assertTrue("API1 auth types should be preserved or enhanced", + api1.getAllAuthTypesFound().size() >= api1InitialAuth.size()); + + assertTrue("API2 auth types should be preserved or enhanced", + api2.getAllAuthTypesFound().size() >= api2InitialAuth.size()); + + // Execute bulk write + if (!updates.isEmpty()) { + ApiInfoDao.instance.getMCollection().bulkWrite(updates); + } + + // Assert DB state + ApiInfo dbApi1 = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(api1.getId())); + ApiInfo dbApi2 = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(api2.getId())); + + assertNotNull("DB API1 should exist", dbApi1); + assertNotNull("DB API2 should exist", dbApi2); + + assertEquals("In-memory and DB auth types should match for API1", + api1.getAllAuthTypesFound(), + dbApi1.getAllAuthTypesFound()); + + assertEquals("In-memory and DB auth types should match for API2", + api2.getAllAuthTypesFound(), + dbApi2.getAllAuthTypesFound()); + } + + /** + * Test Group 4: Edge Cases + * Tests scenarios like missing sample data, no auth headers, multiple samples + */ + @Test + public void testCalcAuthEdgeCases() { + // Clear collections + ApiInfoDao.instance.getMCollection().drop(); + SampleDataDao.instance.getMCollection().drop(); + + // Setup: Create APIs for edge case testing + List apiInfoList = new ArrayList<>(); + + // API 1: Has JWT auth, but NO sample data in DB (should be skipped) + ApiInfo api1 = createApiInfoWithSingleAuthSet(); + api1.getId().setUrl("/api/test/no-sample"); + apiInfoList.add(api1); + // Intentionally NOT inserting sample data for this API + + // API 2: Has sample data with no auth headers + ApiInfo api2 = createApiInfoWithNoAuth(); + api2.getId().setUrl("/api/test/no-auth-header"); + apiInfoList.add(api2); + SampleDataDao.instance.insertOne(createSampleDataWithNoAuth( + api2.getId().getApiCollectionId(), + api2.getId().getUrl(), + api2.getId().getMethod() + )); + + ApiInfoDao.instance.insertMany(apiInfoList); + + // Store initial state + Set> api1InitialAuth = api1.getAllAuthTypesFound() != null + ? new HashSet<>(api1.getAllAuthTypesFound()) + : null; + + // Execute: Call calcAuth + List> updates = CustomAuthUtil.calcAuth(apiInfoList, null, false); + + // Assert: API without sample data should be skipped + // Based on line 118 of CustomAuthUtil: "continue" when !sampleProcessed + // So we expect only 1 update (for api2) + assertEquals("Should have 1 WriteModel update (api1 skipped)", 1, updates.size()); + + // API1 should maintain its initial auth types (unchanged) + if (api1InitialAuth != null) { + assertEquals("API1 auth types should be unchanged", api1InitialAuth, api1.getAllAuthTypesFound()); + } + + // Execute bulk write + if (!updates.isEmpty()) { + ApiInfoDao.instance.getMCollection().bulkWrite(updates); + } + + // Assert DB state: api1 should not be in DB (never inserted with updates) + // api2 should be in DB with detected auth types + ApiInfo dbApi2 = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(api2.getId())); + assertNotNull("DB API2 should exist", dbApi2); + assertEquals("In-memory and DB auth types should match for API2", + api2.getAllAuthTypesFound(), + dbApi2.getAllAuthTypesFound()); + } + + /** + * Test: Existing Auth Types Preserved and New Ones Added + * Tests that when an API has existing auth types [[JWT], [BASIC, AUTHORIZATION_HEADER]], + * and new samples with API_KEY and MTLS are processed, the result is: + * [[JWT], [BASIC, AUTHORIZATION_HEADER], [API_KEY], [MTLS]] + */ + @Test + public void testCalcAuthWithExistingAuthTypesAndNewSamples() { + // Clear collections + ApiInfoDao.instance.getMCollection().drop(); + SampleDataDao.instance.getMCollection().drop(); + + // Setup: Create API with initial auth types [[JWT], [BASIC, AUTHORIZATION_HEADER]] + List apiInfoList = new ArrayList<>(); + ApiInfo api = createApiInfoForPreserveAndEnhance(); + apiInfoList.add(api); + + // Insert 1 SampleData document with 2 samples: one with API_KEY, one with MTLS + SampleDataDao.instance.insertOne(createSampleDataWithApiKeyAndMtlsSamples( + api.getId().getApiCollectionId(), + api.getId().getUrl(), + api.getId().getMethod() + )); + + ApiInfoDao.instance.insertMany(apiInfoList); + + // Store initial auth types for comparison + Set> initialAuth = new HashSet<>(api.getAllAuthTypesFound()); + int initialSetCount = initialAuth.size(); // Should be 2 + + // Execute: Call calcAuth + List> updates = CustomAuthUtil.calcAuth(apiInfoList, null, false); + + // Assert in-memory state: Should have 1 update + assertEquals("Should have 1 WriteModel update", 1, updates.size()); + + // Verify auth types are not null or empty + assertNotNull("Auth types should not be null", api.getAllAuthTypesFound()); + assertFalse("Auth types should not be empty", api.getAllAuthTypesFound().isEmpty()); + + // Verify all initial auth types are preserved + assertTrue("Should preserve all initial auth types", + api.getAllAuthTypesFound().containsAll(initialAuth)); + + // Verify new auth types were added + boolean hasApiKey = api.getAllAuthTypesFound().stream() + .anyMatch(set -> set.contains(ApiInfo.AuthType.API_KEY)); + assertTrue("Should detect API_KEY from samples", hasApiKey); + + boolean hasMtls = api.getAllAuthTypesFound().stream() + .anyMatch(set -> set.contains(ApiInfo.AuthType.MTLS)); + assertTrue("Should detect MTLS from samples", hasMtls); + + // Verify we now have 4 sets total (2 initial + 2 new) + assertEquals("Should have 4 auth type sets total", 4, api.getAllAuthTypesFound().size()); + + // Verify the specific sets exist + Set jwtSet = new HashSet<>(); + jwtSet.add(ApiInfo.AuthType.JWT); + assertTrue("Should have [JWT] set", api.getAllAuthTypesFound().contains(jwtSet)); + + Set basicAuthSet = new HashSet<>(); + basicAuthSet.add(ApiInfo.AuthType.BASIC); + basicAuthSet.add(ApiInfo.AuthType.AUTHORIZATION_HEADER); + assertTrue("Should have [BASIC, AUTHORIZATION_HEADER] set", + api.getAllAuthTypesFound().contains(basicAuthSet)); + + Set apiKeySet = new HashSet<>(); + apiKeySet.add(ApiInfo.AuthType.API_KEY); + assertTrue("Should have [API_KEY] set", api.getAllAuthTypesFound().contains(apiKeySet)); + + Set mtlsSet = new HashSet<>(); + mtlsSet.add(ApiInfo.AuthType.MTLS); + assertTrue("Should have [MTLS] set", api.getAllAuthTypesFound().contains(mtlsSet)); + + // Execute bulk write + if (!updates.isEmpty()) { + ApiInfoDao.instance.getMCollection().bulkWrite(updates); + } + + // Assert DB state + ApiInfo dbApi = ApiInfoDao.instance.findOne(ApiInfoDao.getFilter(api.getId())); + assertNotNull("DB API should exist", dbApi); + assertEquals("In-memory and DB auth types should match", + api.getAllAuthTypesFound(), + dbApi.getAllAuthTypesFound()); + assertEquals("DB should have 4 auth type sets", 4, dbApi.getAllAuthTypesFound().size()); + } + +}