diff --git a/java/registry/pom.xml b/java/registry/pom.xml
index e2285246c..7bf99a6cd 100644
--- a/java/registry/pom.xml
+++ b/java/registry/pom.xml
@@ -25,7 +25,7 @@
0.3.1
1.8
1.8
- 0.8.1
+ 0.8.11
6.6.0
2.0.3
@@ -466,7 +466,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.0.0-M5
+ 3.3.0
@@ -570,17 +570,21 @@
dev/sunbirdrc/pojos/*
**/I*.java
+ org/drools/**/*
org.apache.maven.plugins
maven-surefire-plugin
+ 3.3.0
+ methods
+
${runSuite}
- 0
+ 1
diff --git a/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java b/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java
index 5ea49b79b..6953f7f45 100644
--- a/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java
+++ b/java/registry/src/main/java/dev/sunbirdrc/registry/service/SchemaService.java
@@ -177,6 +177,7 @@ private void ensureCredentialSchema(String title, Object credentialTemplate, Str
if(!signatureEnabled || !Objects.equals(signatureProvider, SignatureV2ServiceImpl.class.getName())) {
return;
}
+ if(credentialTemplate == null || credentialTemplate == "") return;
try {
credentialSchemaService.ensureCredentialSchema(title, credentialTemplate, status);
} catch (Exception e) {
diff --git a/java/registry/src/main/resources/application.yml b/java/registry/src/main/resources/application.yml
index cff05e9c3..5b709c9bf 100644
--- a/java/registry/src/main/resources/application.yml
+++ b/java/registry/src/main/resources/application.yml
@@ -474,12 +474,12 @@ signature:
# suffixSeperator : specifies the separator used between entity name and suffix. If audit schema name is Teacher_Audit.json, the suffixSeperator is _.
audit:
- enabled: ${audit_enabled:false}
- vc-enabled: ${audit_vc_enabled:false}
+ enabled: true
+ vc-enabled: true
frame:
- store: ${audit_frame_store:DATABASE}
- suffix: ${audit_suffix:Audit}
- suffixSeparator: ${audit_suffixSeparator:_}
+ store: DATABASE
+ suffix: Audit
+ suffixSeparator: _
authentication:
enabled: ${authentication_enabled:true}
@@ -546,3 +546,9 @@ elastic:
# elastic-search connection info
connection_url: ${elastic_search_connection_url:localhost:9200}
+idgen:
+ enabled: true
+ tenantId: default
+ healthCheckURL: http://localhost:8088/egov-idgen/health
+ generateURL: http://localhost:8088/egov-idgen/id/_generate
+ idFormatURL: http://localhost:8088/egov-idgen/id/_format/add
diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/AuditServiceImplTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/AuditServiceImplTest.java
new file mode 100644
index 000000000..fc96f4d03
--- /dev/null
+++ b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/AuditServiceImplTest.java
@@ -0,0 +1,186 @@
+package dev.sunbirdrc.registry.service.impl;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.sunbirdrc.pojos.AuditInfo;
+import dev.sunbirdrc.pojos.AuditRecord;
+import dev.sunbirdrc.registry.exception.AuditFailedException;
+import dev.sunbirdrc.registry.helper.SignatureHelper;
+import dev.sunbirdrc.registry.middleware.util.Constants;
+import dev.sunbirdrc.registry.service.IAuditService;
+import dev.sunbirdrc.registry.sink.shard.Shard;
+import dev.sunbirdrc.registry.util.Definition;
+import dev.sunbirdrc.registry.util.IDefinitionsManager;
+import dev.sunbirdrc.registry.util.OSSystemFieldsHelper;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.List;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = {ObjectMapper.class})
+@ActiveProfiles(Constants.TEST_ENVIRONMENT)
+public class AuditServiceImplTest {
+
+ @Value("${audit.enabled}")
+ private boolean auditEnabled;
+
+ @Value("${audit.frame.store}")
+ private String auditFrameStore;
+
+ @Value("${audit.frame.suffix}")
+ private String auditSuffix;
+
+ @Value("${audit.frame.suffixSeparator}")
+ private String auditSuffixSeparator;
+
+ @Value("${audit.vc-enabled:false}")
+ private boolean auditVCEnabled;
+
+ @Mock
+ private IDefinitionsManager definitionsManager;
+
+ @Mock
+ private OSSystemFieldsHelper systemFieldsHelper;
+
+ @Mock
+ private AuditProviderFactory auditProviderFactory;
+
+ @Mock
+ private SignatureHelper signatureHelper;
+
+ @Mock
+ private ObjectMapper objectMapper;
+
+ @InjectMocks
+ private AuditServiceImpl auditService;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ setField(auditService, "auditEnabled", auditEnabled);
+ setField(auditService, "auditFrameStore", auditFrameStore);
+ setField(auditService, "auditSuffix", auditSuffix);
+ setField(auditService, "auditSuffixSeparator", auditSuffixSeparator);
+ setField(auditService, "auditVCEnabled", auditVCEnabled);
+ }
+
+ private void setField(Object targetObject, String fieldName, Object value) throws Exception {
+ Field field = targetObject.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(targetObject, value);
+ }
+
+ @Test
+ public void shouldAudit_ReturnsTrueWhenFileAuditEnabled() throws Exception {
+ setField(auditService, "auditFrameStore", Constants.FILE);
+ boolean result = auditService.shouldAudit("EntityType");
+ assertTrue(result);
+ }
+
+ @Test
+ public void shouldAudit_ReturnsTrueWhenDBAuditEnabledAndDefinitionNotNull() throws Exception {
+ when(definitionsManager.getDefinition(anyString())).thenReturn(mock(Definition.class));
+ setField(auditService, "auditFrameStore", Constants.DATABASE);
+ boolean result = auditService.shouldAudit("EntityType");
+ assertTrue(result);
+ }
+
+ @Test
+ public void shouldAudit_ReturnsFalseWhenNoAuditingEnabled() throws Exception {
+ setField(auditService, "auditEnabled", false);
+ boolean result = auditService.shouldAudit("EntityType");
+ assertFalse(result);
+ }
+
+ @Test
+ public void isAuditAction_ReturnsAuditActionForMatchingSuffix() {
+ String entityType = auditSuffix;
+ String action = auditService.isAuditAction(entityType);
+ assertEquals(Constants.AUDIT_ACTION_AUDIT, action);
+ }
+
+ @Test
+ public void isAuditAction_ReturnsSearchActionForNonMatchingSuffix() {
+ String entityType = "NonMatchingEntity";
+ String action = auditService.isAuditAction(entityType);
+ assertEquals(Constants.AUDIT_ACTION_SEARCH, action);
+ }
+
+ @Test
+ public void createAuditInfo_ReturnsAuditInfoList() {
+ String auditAction = "AuditAction";
+ String entityType = "EntityType";
+ List auditInfoList = auditService.createAuditInfo(auditAction, entityType);
+
+ // Then
+ assertEquals(1, auditInfoList.size());
+ assertEquals(auditAction, auditInfoList.get(0).getOp());
+ assertEquals("/" + entityType, auditInfoList.get(0).getPath());
+ }
+
+ @Test
+ public void convertAuditRecordToJson_ReturnsJsonNode() throws IOException {
+ // Given
+ AuditRecord auditRecord = new AuditRecord();
+ JsonNode inputNode = mock(JsonNode.class);
+ Shard shard = mock(Shard.class);
+ String vertexLabel = "VertexLabel";
+ when(objectMapper.writeValueAsString(any())).thenReturn("{}");
+
+ // When
+ JsonNode result = auditService.convertAuditRecordToJson(auditRecord, vertexLabel);
+
+ // Then
+ assertNotNull(result);
+ assertTrue(result.has(vertexLabel));
+ }
+
+ @Test
+ public void createAuditInfoWithJson_ReturnsAuditInfoListFromJson() throws JsonProcessingException {
+ // Given
+ String auditAction = "AuditAction";
+ JsonNode differenceJson = mock(JsonNode.class);
+ String entityType = "EntityType";
+ when(objectMapper.treeToValue(any(JsonNode.class), eq(AuditInfo[].class)))
+ .thenReturn(new AuditInfo[]{new AuditInfo()});
+
+ // When
+ List auditInfoList = auditService.createAuditInfoWithJson(auditAction, differenceJson, entityType);
+
+ // Then
+ assertNotNull(auditInfoList);
+ assertEquals(1, auditInfoList.size());
+ }
+
+ @Test
+ public void testDoAudit() throws AuditFailedException {
+ // Given
+ AuditRecord auditRecord = mock(AuditRecord.class);
+ JsonNode inputNode = mock(JsonNode.class);
+ Shard shard = mock(Shard.class);
+ IAuditService auditProvider = mock(IAuditService.class);
+
+ when(auditProviderFactory.getAuditService(anyString())).thenReturn(auditProvider);
+
+ // When
+ auditService.doAudit(auditRecord, inputNode, shard);
+
+ // Then
+ verify(auditProvider, times(1)).doAudit(auditRecord, inputNode, shard);
+ }
+}
+
diff --git a/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/IdGenServiceTest.java b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/IdGenServiceTest.java
new file mode 100644
index 000000000..5e4f40769
--- /dev/null
+++ b/java/registry/src/test/java/dev/sunbirdrc/registry/service/impl/IdGenServiceTest.java
@@ -0,0 +1,168 @@
+package dev.sunbirdrc.registry.service.impl;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import dev.sunbirdrc.pojos.ComponentHealthInfo;
+import dev.sunbirdrc.pojos.SunbirdRCInstrumentation;
+import dev.sunbirdrc.pojos.UniqueIdentifierField;
+import dev.sunbirdrc.registry.exception.CustomException;
+import dev.sunbirdrc.registry.exception.UniqueIdentifierException.GenerateException;
+import dev.sunbirdrc.registry.exception.UniqueIdentifierException.IdFormatException;
+import dev.sunbirdrc.registry.exception.UniqueIdentifierException.UnreachableException;
+import dev.sunbirdrc.registry.middleware.util.Constants;
+import dev.sunbirdrc.registry.middleware.util.JSONUtil;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.*;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.web.client.ResourceAccessException;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.*;
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = {ObjectMapper.class})
+@ActiveProfiles(Constants.TEST_ENVIRONMENT)
+public class IdGenServiceTest {
+
+ @Value("${idgen.generateURL}")
+ private String generateUrl;
+
+ @Value("${idgen.idFormatURL}")
+ private String idFormatUrl;
+
+ @Value("${idgen.healthCheckURL}")
+ private String healthCheckUrl;
+
+ @Value("${idgen.tenantId}")
+ private String tenantId;
+
+ @Value("${idgen.enabled}")
+ private boolean enabled;
+
+ @Mock
+ private Gson gson;
+
+ @Mock
+ private SunbirdRCInstrumentation watch;
+
+ @Mock
+ private RetryRestTemplate retryRestTemplate;
+
+ @InjectMocks
+ private IdGenService idGenService;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ // Manually set the injected values in the mock object
+ setField(idGenService, "generateUrl", generateUrl);
+ setField(idGenService, "idFormatUrl", idFormatUrl);
+ setField(idGenService, "healthCheckUrl", healthCheckUrl);
+ setField(idGenService, "tenantId", tenantId);
+ setField(idGenService, "enabled", enabled);
+ }
+
+ private void setField(Object targetObject, String fieldName, Object value) throws Exception {
+ Field field = targetObject.getClass().getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(targetObject, value);
+ }
+
+ @Test
+ public void testGenerateIdSuccessful() throws CustomException, IOException {
+ List fields = new ArrayList<>();
+ UniqueIdentifierField field1 = new UniqueIdentifierField();
+ field1.setField("field1");
+ fields.add(field1);
+
+ HttpEntity entity = new HttpEntity<>("request");
+
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(eq(generateUrl), any(HttpEntity.class))).thenReturn(new ResponseEntity<>("{\"responseInfo\":{\"status\":\"SUCCESSFUL\"},\"idResponses\":[{\"id\":\"1234\"}]}", HttpStatus.OK));
+
+ Map result = idGenService.generateId(fields);
+
+ assertNotNull(result);
+ assertEquals("1234", result.get("field1"));
+ }
+
+ @Test(expected = GenerateException.class)
+ public void testGenerateIdFailure() throws CustomException, IOException {
+ List fields = new ArrayList<>();
+
+ HttpEntity entity = new HttpEntity<>("request");
+
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(any(), eq(entity))).thenReturn(new ResponseEntity<>("{\"responseInfo\":{\"status\":\"FAILED\"}}", HttpStatus.OK));
+
+ idGenService.generateId(fields);
+ }
+
+ @Test(expected = UnreachableException.class)
+ public void testGenerateIdResourceAccessException() throws CustomException {
+ List fields = new ArrayList<>();
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(eq(generateUrl), any(HttpEntity.class))).thenThrow(new ResourceAccessException("Exception"));
+
+ idGenService.generateId(fields);
+ }
+
+ @Test
+ public void testSaveIdFormatSuccessful() throws CustomException, IOException {
+ List fields = new ArrayList<>();
+
+ HttpEntity entity = new HttpEntity<>("request");
+
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(eq(idFormatUrl), any(HttpEntity.class))).thenReturn(new ResponseEntity<>("{\"responseInfo\":{\"status\":\"SUCCESSFUL\"}}", HttpStatus.OK));
+ idGenService.saveIdFormat(fields);
+ }
+
+ @Test(expected = GenerateException.class)
+ public void testSaveIdFormatFailure() throws CustomException, IOException {
+ List fields = new ArrayList<>();
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(eq(idFormatUrl), any(HttpEntity.class))).thenReturn(new ResponseEntity<>("{\"responseInfo\":{\"status\":\"FAILED\"},\"errorMsgs\":[\"Some error\"]}", HttpStatus.OK));
+ idGenService.saveIdFormat(fields);
+ }
+
+ @Test(expected = UnreachableException.class)
+ public void testSaveIdFormatResourceAccessException() throws CustomException {
+ List fields = new ArrayList<>();
+ when(gson.toJson(anyMap())).thenReturn("request");
+ when(retryRestTemplate.postForEntity(eq(idFormatUrl), any(HttpEntity.class))).thenThrow(new ResourceAccessException("Exception"));
+ idGenService.saveIdFormat(fields);
+ }
+
+ @Test
+ public void testGetHealthInfoWhenHealthy() throws IOException {
+ when(retryRestTemplate.getForEntity(any())).thenReturn(new ResponseEntity<>("{\"status\":\"UP\"}", HttpStatus.OK));
+ ComponentHealthInfo healthInfo = idGenService.getHealthInfo();
+ assertNotNull(healthInfo);
+ assertTrue(healthInfo.isHealthy());
+ }
+
+ @Test
+ public void testGetHealthInfoWhenUnhealthy() throws IOException {
+ when(retryRestTemplate.getForEntity(any())).thenReturn(new ResponseEntity<>("{\"status\":\"DOWN\"}", HttpStatus.OK));
+ ComponentHealthInfo healthInfo = idGenService.getHealthInfo();
+ assertNotNull(healthInfo);
+ assertFalse(healthInfo.isHealthy());
+ }
+}
diff --git a/services/credential-schema/package.json b/services/credential-schema/package.json
index cee39660c..b2c9c7a68 100644
--- a/services/credential-schema/package.json
+++ b/services/credential-schema/package.json
@@ -86,7 +86,9 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
- "**/*.(t|j)s"
+ "**/*.(t|j)s",
+ "!**/*.module.ts",
+ "!**/main.ts"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
diff --git a/services/credential-schema/src/app.controller.spec.ts b/services/credential-schema/src/app.controller.spec.ts
new file mode 100644
index 000000000..822c7055c
--- /dev/null
+++ b/services/credential-schema/src/app.controller.spec.ts
@@ -0,0 +1,86 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { AppController } from './app.controller';
+import { HealthCheckService, HttpHealthIndicator } from '@nestjs/terminus';
+import { PrismaHealthIndicator } from './utils/prisma.health';
+
+describe('AppController', () => {
+ let appController: AppController;
+ let healthCheckService: HealthCheckService;
+ let prismaIndicator: PrismaHealthIndicator;
+ let http: HttpHealthIndicator;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [AppController],
+ providers: [
+ {
+ provide: HealthCheckService,
+ useValue: {
+ check: jest.fn(),
+ },
+ },
+ {
+ provide: PrismaHealthIndicator,
+ useValue: {
+ isHealthy: jest.fn(),
+ },
+ },
+ {
+ provide: HttpHealthIndicator,
+ useValue: {
+ responseCheck: jest.fn(),
+ },
+ },
+ ],
+ }).compile();
+
+ appController = module.get(AppController);
+ healthCheckService = module.get(HealthCheckService);
+ prismaIndicator = module.get(PrismaHealthIndicator);
+ http = module.get(HttpHealthIndicator);
+ });
+
+ it('should be defined', () => {
+ expect(appController).toBeDefined();
+ });
+
+ describe('checkHealth', () => {
+ it('should return health check result', async () => {
+ const result = { status: 'ok', info: {}, error: {}, details: {} };
+ (healthCheckService.check as jest.Mock).mockResolvedValue(result);
+ (prismaIndicator.isHealthy as jest.Mock).mockResolvedValue({ db: { status: 'up' } });
+ (http.responseCheck as jest.Mock).mockResolvedValue({ 'identity-service': { status: 'up' } });
+
+ expect(await appController.checkHealth()).toBe(result);
+ expect(healthCheckService.check).toHaveBeenCalledWith([
+ expect.any(Function),
+ expect.any(Function),
+ ]);
+ });
+
+ it('should call prismaIndicator.isHealthy', async () => {
+ (healthCheckService.check as jest.Mock).mockImplementation(async (indicators) => {
+ await indicators[0]();
+ await indicators[1]();
+ });
+
+ await appController.checkHealth();
+ expect(prismaIndicator.isHealthy).toHaveBeenCalledWith('db');
+ });
+
+ it('should call http.responseCheck with correct arguments', async () => {
+ process.env.IDENTITY_BASE_URL = 'http://identity-service-url';
+ (healthCheckService.check as jest.Mock).mockImplementation(async (indicators) => {
+ await indicators[0]();
+ await indicators[1]();
+ });
+
+ await appController.checkHealth();
+ expect(http.responseCheck).toHaveBeenCalledWith(
+ 'identity-service',
+ 'http://identity-service-url/health',
+ expect.any(Function)
+ );
+ });
+ });
+});
diff --git a/services/credential-schema/src/auth/auth.gaurd.spec.ts b/services/credential-schema/src/auth/auth.gaurd.spec.ts
new file mode 100644
index 000000000..d8ac362fa
--- /dev/null
+++ b/services/credential-schema/src/auth/auth.gaurd.spec.ts
@@ -0,0 +1,80 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { AuthGuard } from './auth.guard';
+import { Reflector } from '@nestjs/core';
+import { ConfigService } from '@nestjs/config';
+
+describe('AuthGuard', () => {
+ let guard: AuthGuard;
+ let reflector: Reflector;
+ let configService: ConfigService;
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeAll(async () => {
+ jest.mock('jwks-rsa', () => ({
+ __esModule: true,
+ default: jest.fn(),
+ }));
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ AuthGuard,
+ {
+ provide: Reflector,
+ useValue: {
+ get: jest.fn(),
+ },
+ },
+ {
+ provide: ConfigService,
+ useValue: {
+ get: jest.fn(),
+ },
+ },
+ ],
+ }).compile();
+
+ guard = module.get(AuthGuard);
+ reflector = module.get(Reflector);
+ configService = module.get(ConfigService);
+ });
+
+ beforeEach(async () => {
+ originalEnv = { ...process.env };
+ process.env.ENABLE_AUTH = 'true';
+ jest.restoreAllMocks();
+ })
+
+ it('should be defined', () => {
+ expect(guard).toBeDefined();
+ });
+
+ describe('canActivate', () => {
+ it('should return true if isPublic is set to true', async () => {
+ jest.spyOn(reflector, 'get').mockReturnValue(true);
+ const result = await guard.canActivate({ getHandler: jest.fn() });
+ expect(result).toEqual(true);
+ });
+
+ it('should return true if ENABLE_AUTH is false', async () => {
+ process.env.ENABLE_AUTH = 'false';
+ jest.spyOn(reflector, 'get').mockReturnValue(false);
+ jest.spyOn(configService, 'get').mockReturnValue('false');
+ const result = await guard.canActivate({ getHandler: jest.fn() });
+ expect(result).toEqual(true);
+ });
+
+ it('should return false if no Bearer token found', async () => {
+ jest.spyOn(reflector, 'get').mockReturnValue(false);
+ jest.spyOn(configService, 'get').mockReturnValue('true');
+ const request = { headers: { } };
+ const result = await guard.canActivate({ getHandler: jest.fn(), switchToHttp: () => ({ getRequest: () => request }) });
+ expect(result).toEqual(false);
+ });
+
+ // Add more test cases as needed to cover different scenarios
+ });
+
+ afterEach(() => {
+ // Restore the original process.env after the test
+ process.env = { ...originalEnv };
+ });
+});
diff --git a/services/credential-schema/src/schema/schema.fixtures.ts b/services/credential-schema/src/schema/schema.fixtures.ts
index 22f2d5573..59bb9ce9b 100644
--- a/services/credential-schema/src/schema/schema.fixtures.ts
+++ b/services/credential-schema/src/schema/schema.fixtures.ts
@@ -1,5 +1,6 @@
import { randomUUID } from 'crypto';
import { CreateCredentialDTO } from './dto/create-credentials.dto';
+import { VerifiableCredentialSchema } from '@prisma/client';
export const generateCredentialSchemaTestBody = (): CreateCredentialDTO => {
return {
@@ -66,3 +67,41 @@ export const generateTestDIDBody = () => {
],
};
};
+
+export const testSchemaRespose1: VerifiableCredentialSchema = {
+ id: 'schema-id',
+ type: 'UniversityDegreeCredential',
+ version: '1.0.0',
+ name: 'Bachelor of Science in Computer Science',
+ author: 'University A',
+ authored: new Date('2023-01-01'),
+ schema: { title: 'B.Sc. Computer Science', properties: { gpa: { type: 'number' } } },
+ proof: { type: 'Ed25519Signature2020', created: '2023-01-01T00:00:00Z', verificationMethod: 'did:example:123#key-1', proofPurpose: 'assertionMethod', jws: 'eyJhbGciOiJFZERTQSJ9..CLcx8u6ljgfhHGghjGFDhfghg' },
+ createdAt: new Date('2023-01-01T00:00:00Z'),
+ updatedAt: new Date('2023-01-01T00:00:00Z'),
+ createdBy: 'admin',
+ updatedBy: 'admin',
+ deletedAt: null,
+ tags: ['degree', 'computer science', 'bachelor'],
+ status: 'DRAFT',
+ deprecatedId: null,
+};
+
+export const testSchemaRespose2: VerifiableCredentialSchema = {
+ id: 'schema-id',
+ type: 'ProfessionalCertification',
+ version: '1.1.0',
+ name: 'Certified Blockchain Expert',
+ author: 'Blockchain Institute',
+ authored: new Date('2023-02-01'),
+ schema: { title: 'Blockchain Certification', properties: { certificationId: { type: 'string' }, issuedDate: { type: 'string' } } },
+ proof: { type: 'Ed25519Signature2020', created: '2023-02-01T00:00:00Z', verificationMethod: 'did:example:456#key-1', proofPurpose: 'assertionMethod', jws: 'eyJhbGciOiJFZERTQSJ9..mHkj8oOlgfhHGghjGFDhfghg' },
+ createdAt: new Date('2023-02-01T00:00:00Z'),
+ updatedAt: new Date('2023-02-01T00:00:00Z'),
+ createdBy: 'admin',
+ updatedBy: 'admin',
+ deletedAt: null,
+ tags: ['certification', 'blockchain', 'expert'],
+ status: 'DRAFT',
+ deprecatedId: null,
+};
\ No newline at end of file
diff --git a/services/credential-schema/src/schema/schema.service.spec.ts b/services/credential-schema/src/schema/schema.service.spec.ts
index 2392fd1fc..244e3efae 100644
--- a/services/credential-schema/src/schema/schema.service.spec.ts
+++ b/services/credential-schema/src/schema/schema.service.spec.ts
@@ -1,25 +1,33 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SchemaService } from './schema.service';
import { UtilsService } from '../utils/utils.service';
-import { PrismaClient } from '@prisma/client';
+import { PrismaClient, SchemaStatus } from '@prisma/client';
import { HttpModule } from '@nestjs/axios';
import {
generateCredentialSchemaTestBody,
- generateTestDIDBody,
+ generateTestDIDBody, testSchemaRespose1, testSchemaRespose2
} from './schema.fixtures';
+import { BadRequestException, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common';
+import { GetCredentialSchemaDTO } from './dto/getCredentialSchema.dto';
describe('SchemaService', () => {
let service: SchemaService;
+ let prisma: PrismaClient;
let utilsService: UtilsService;
- beforeEach(async () => {
+ beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule],
providers: [SchemaService, UtilsService, PrismaClient],
}).compile();
service = module.get(SchemaService);
+ prisma = module.get(PrismaClient);
utilsService = module.get(UtilsService);
});
+ beforeEach(async () => {
+ jest.restoreAllMocks();
+ })
+
it('should be defined', () => {
expect(service).toBeDefined();
expect(utilsService).toBeDefined();
@@ -34,8 +42,8 @@ describe('SchemaService', () => {
credSchemaPayload,
);
expect(vcCredSchema).toBeDefined();
- expect(vcCredSchema.schema.proof).toBeTruthy();
- const getVCCredSchema = await service.getCredentialSchemaByIdAndVersion({
+ // expect(vcCredSchema.schema.proof).toBeTruthy();
+ const getVCCredSchema: GetCredentialSchemaDTO = await service.getCredentialSchemaByIdAndVersion({
id_version: {
id: vcCredSchema.schema.id,
version: '1.0.0',
@@ -55,7 +63,7 @@ describe('SchemaService', () => {
credSchemaPayload,
);
expect(vcCredSchema).toBeDefined();
- expect(vcCredSchema.schema.proof).toBeTruthy();
+ // expect(vcCredSchema.schema.proof).toBeTruthy();
const getVCCredSchema = await service.getCredentialSchemaByIdAndVersion({
id_version: {
id: vcCredSchema.schema.id,
@@ -177,4 +185,149 @@ describe('SchemaService', () => {
expect(updatedSchema.status).toBe('PUBLISHED');
expect(updatedSchema.schema.author).toBe(pschema.schema.id);
});
+ describe('should try to update state of a schema', () => {
+ // create new schema
+ let didBody;
+ let did;
+ let credSchemaPayload;
+ const updateSchemaState = async (state: string, schema) => {
+ const updateSchemaPayload = {
+ schema: null,
+ status: state,
+ tags: ['test1', 'test2'],
+ };
+ return await service.updateCredentialSchema(
+ {
+ id_version: { id: schema.schema.id, version: schema.schema.version },
+ },
+ updateSchemaPayload,
+ );
+ }
+ const testCases = [
+ { input: 'DRAFT', expected: 'DRAFT' },
+ { input: 'DEPRECATED', expected: 'DEPRECATED' },
+ { input: 'PUBLISHED', expected: 'PUBLISHED' },
+ { input: 'REVOKED', expected: 'REVOKED' },
+ ];
+
+ beforeAll(async () => {
+ didBody = generateTestDIDBody();
+ did = await utilsService.generateDID(didBody);
+ credSchemaPayload = generateCredentialSchemaTestBody();
+ credSchemaPayload.schema.author = did.id;
+ })
+
+ test.each(testCases)("Update to a $expected schema", async ({ input, expected }) => {
+ const schema = await service.createCredentialSchema(credSchemaPayload);
+ const updatedSchema = await updateSchemaState(input, schema);
+ expect(updatedSchema).toBeDefined();
+ expect(updatedSchema.status).toEqual(expected);
+ expect(updatedSchema.schema.version).toBe('1.0.1');
+ });
+
+ it(`Update REVOKED schema status`, async () => {
+ const schema= await service.createCredentialSchema(credSchemaPayload);
+ const revokedSchema = await updateSchemaState('REVOKED', schema);
+ (['DRAFT', 'DEPRECATED', 'REVOKED', 'PUBLISHED'].forEach(state => {
+ expect(() => {
+ return updateSchemaState(state, revokedSchema);
+ }).rejects.toThrow(
+ `Schema with id: ${revokedSchema.schema.id} and version: ${revokedSchema.schema.version} is already revoked`,
+ );
+ }));
+ });
+
+ it('Update schema with invalid id', async () => {
+ await expect(() => updateSchemaState('UNKNOWN', { schema: {id: undefined, version: "1.0.0"} })).rejects
+ .toThrow('Error fetching schema for update from db');
+ await expect(() => service.updateSchemaStatus({
+ id_version: { id: undefined, version: '123' },
+ }, 'PUBLISHED')).rejects
+ .toThrow('Error fetching schema for update from db');
+ });
+
+ it('Update schema with not existing id', async () => {
+ await expect(() => updateSchemaState('UNKNOWN', { schema: {id: '123', version: "1.0.0"} })).rejects
+ .toThrow(NotFoundException);
+ });
+
+ it('Update to a UNKNOWN schema', async () => {
+ const schema = await service.createCredentialSchema(credSchemaPayload);
+ await expect(() => updateSchemaState('UNKNOWN', schema)).rejects
+ .toThrow(InternalServerErrorException);
+ });
+ });
+
+ describe('getAllSchemasById', () => {
+ it('should return formatted schemas', async () => {
+
+ const id = 'schema-id';
+ const mockSchemas = [
+ testSchemaRespose1,
+ testSchemaRespose2,
+ ];
+ jest.spyOn(prisma.verifiableCredentialSchema, 'findMany').mockResolvedValue(mockSchemas);
+
+ const result = await service.getAllSchemasById(id);
+
+ expect(prisma.verifiableCredentialSchema.findMany).toHaveBeenCalledWith({ where: { id } });
+ expect(result).toHaveLength(2);
+ expect(result[0].schema.id).toEqual(id);
+ expect(result[1].schema.id).toEqual(id);
+ });
+
+ it('should throw an InternalServerErrorException if there is an error', async () => {
+ const id = 'some-id';
+ jest.spyOn(prisma.verifiableCredentialSchema, 'findMany').mockRejectedValue(new Error());
+
+ await expect(service.getAllSchemasById(id)).rejects.toThrow(InternalServerErrorException);
+ });
+ });
+
+ describe('getSchemaByTags', () => {
+ it('should return formatted schemas with pagination', async () => {
+ const tags = ['tag1', 'tag2'];
+ const page = 1;
+ const limit = 10;
+ const mockSchemas = [
+ testSchemaRespose1,
+ testSchemaRespose2,
+ ];
+ jest.spyOn(prisma.verifiableCredentialSchema, 'findMany').mockResolvedValue(mockSchemas);
+
+ const result = await service.getSchemaByTags(tags, page, limit);
+
+ expect(prisma.verifiableCredentialSchema.findMany).toHaveBeenCalledWith({
+ where: { tags: { hasSome: tags } },
+ skip: (page - 1) * limit,
+ take: limit,
+ });
+ expect(result).toEqual(mockSchemas.map((schema) => ({
+ schema: {
+ type: schema.type,
+ id: schema.id,
+ version: schema.version,
+ name: schema.name,
+ author: schema.author,
+ authored: schema.authored,
+ schema: schema.schema,
+ proof: schema.proof,
+ },
+ tags: schema.tags,
+ status: schema.status,
+ createdAt: schema.createdAt,
+ updatedAt: schema.updatedAt,
+ createdBy: schema.createdBy,
+ updatedBy: schema.updatedBy,
+ deprecatedId: schema.deprecatedId,
+ })));
+ });
+
+ it('should throw an InternalServerErrorException if there is an error', async () => {
+ const tags = ['tag1', 'tag2'];
+ jest.spyOn(prisma.verifiableCredentialSchema, 'findMany').mockRejectedValue(new Error());
+
+ await expect(service.getSchemaByTags(tags)).rejects.toThrow(InternalServerErrorException);
+ });
+ });
});
diff --git a/services/credential-schema/src/schema/schema.service.ts b/services/credential-schema/src/schema/schema.service.ts
index 511159465..f9a2bbc3a 100644
--- a/services/credential-schema/src/schema/schema.service.ts
+++ b/services/credential-schema/src/schema/schema.service.ts
@@ -58,7 +58,7 @@ export class SchemaService {
createdBy: schema.createdBy,
updatedBy: schema.updatedBy,
deprecatedId: schema.deprecatedId,
- };
+ } as GetCredentialSchemaDTO;
} else {
this.logger.error('schema not found for userInput', userWhereUniqueInput);
throw new NotFoundException('Schema not found');
@@ -186,7 +186,7 @@ export class SchemaService {
authored: credSchema.schema.authored,
schema: credSchema.schema.schema as Prisma.JsonValue,
status: credSchema.status as SchemaStatus,
- proof: credSchema.schema.proof as Prisma.JsonValue,
+ proof: credSchema.schema.proof as Prisma.JsonValue || undefined,
tags: credSchema.tags as string[],
deprecatedId: deprecatedId,
},
@@ -426,45 +426,4 @@ export class SchemaService {
return newSchema;
}
-
- async deprecateSchema(id: string, version: string) {
- let schema: VerifiableCredentialSchema;
- try {
- schema = await this.prisma.verifiableCredentialSchema.findUniqueOrThrow({
- where: {
- id_version: {
- id,
- version,
- },
- },
- });
- } catch (err) {
- this.logger.error('Error fetching schema from the db', err);
- throw new InternalServerErrorException(
- 'Error fetching schema from the db',
- );
- }
-
- if (!schema) {
- throw new NotFoundException(
- `No schema found with the given id: ${id} and version: ${version}`,
- );
- }
-
- try {
- const deprecatedSchema = await this.updateSchemaStatus(
- {
- id_version: {
- id,
- version,
- },
- },
- 'DEPRECATED',
- );
- return this.formatResponse(deprecatedSchema);
- } catch (err) {
- this.logger.error('Error in updating schema status', err);
- throw new InternalServerErrorException('Error in updating schema status');
- }
- }
}
diff --git a/services/credential-schema/src/schema/schemas.ts b/services/credential-schema/src/schema/schemas.ts
deleted file mode 100644
index 3ff0cdbbe..000000000
--- a/services/credential-schema/src/schema/schemas.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as proof_of_alumni from '../../samples/proof_of_alumni.json';
-import * as proof_of_training from '../../samples/proof_of_training.json';
-import * as proof_of_marks from '../../samples/proof_of_marks.json';
-import * as proof_of_academic_evaluation from '../../samples/proof_of_academic_evaluation.json';
-import * as marksheet from '../../samples/marksheet.json';
-
-const schemas = {
- proof_of_academic_evaluation: proof_of_academic_evaluation,
- proof_of_alumni: proof_of_alumni,
- proof_of_marks: proof_of_marks,
- proof_of_training: proof_of_training,
- marksheet: marksheet,
-};
-
-export default schemas;
diff --git a/services/credential-schema/src/utils/mock.util.service.ts b/services/credential-schema/src/utils/mock.util.service.ts
deleted file mode 100644
index 40586efa1..000000000
--- a/services/credential-schema/src/utils/mock.util.service.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-export class UtilsServiceMock {
- async sign(did: string, body: any) {
- const signedVCResponse = {
- data: {
- signed: 'mockSignedValue',
- },
- };
-
- const proof = {
- proofValue: signedVCResponse.data.signed as string,
- proofPurpose: 'assertionMethod',
- created: new Date().toISOString(),
- type: 'Ed25519Signature2020',
- verificationMethod: did,
- };
- return proof;
- }
-
- async generateDID(body: any) {
- const mockGenerateDID = [
- {
- '@context': 'https://w3id.org/did/v1',
- id: 'did:rcw:98bee106-9630-4a31-ba26-177c478431cc',
- alsoKnownAs: ['did.test@gmail.com.test'],
- service: [
- {
- id: 'IdentityHub',
- type: 'IdentityHub',
- serviceEndpoint: {
- '@context': 'schema.identity.foundation/hub',
- '@type': 'UserServiceEndpoint',
- instance: ['did:test:hub.id'],
- },
- },
- ],
- verificationMethod: [
- {
- id: 'auth-key',
- type: 'RS256',
- publicKeyJwk: {
- kty: 'EC',
- crv: 'secp256k1',
- x: 'u6YjEMn37er9zqqS4YTnyaXOuAgJ6hRD2z9tr3Yl5HI',
- y: 'j-BOp476lD9g_Ff0I8-wsENrDtOC4PvDQ0ssAFPTk4g',
- },
- controller: 'did:rcw:98bee106-9630-4a31-ba26-177c478431cc',
- },
- ],
- authentication: ['auth-key'],
- },
- ];
- return mockGenerateDID[0];
- }
-}
diff --git a/services/credential-schema/src/utils/prisma.health.spec.ts b/services/credential-schema/src/utils/prisma.health.spec.ts
new file mode 100644
index 000000000..833b49d8d
--- /dev/null
+++ b/services/credential-schema/src/utils/prisma.health.spec.ts
@@ -0,0 +1,48 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { PrismaHealthIndicator } from './prisma.health';
+import { HealthCheckError } from '@nestjs/terminus';
+import { PrismaClient } from '@prisma/client';
+
+describe('PrismaHealthIndicator', () => {
+ let prismaHealthIndicator: PrismaHealthIndicator;
+ let prismaClient: PrismaClient;
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ PrismaHealthIndicator,
+ {
+ provide: PrismaClient,
+ useValue: {
+ $queryRaw: jest.fn(),
+ },
+ },
+ ],
+ }).compile();
+
+ prismaHealthIndicator = module.get(PrismaHealthIndicator);
+ prismaClient = module.get(PrismaClient);
+ });
+
+ it('should be defined', () => {
+ expect(prismaHealthIndicator).toBeDefined();
+ });
+
+ describe('isHealthy', () => {
+ it('should return health status as up', async () => {
+ (prismaClient.$queryRaw as jest.Mock).mockResolvedValue([1]);
+
+ const result = await prismaHealthIndicator.isHealthy('db');
+ expect(result).toEqual({ db: { status: 'up' } });
+ expect(prismaClient.$queryRaw).toHaveBeenCalledWith([`SELECT 1`]);
+ });
+
+ it('should throw HealthCheckError when query fails', async () => {
+ const error = new Error('Query failed');
+ (prismaClient.$queryRaw as jest.Mock).mockRejectedValue(error);
+
+ await expect(prismaHealthIndicator.isHealthy('db')).rejects.toThrow(HealthCheckError);
+ await expect(prismaHealthIndicator.isHealthy('db')).rejects.toThrow('Prisma health check failed');
+ });
+ });
+});
diff --git a/services/credential-schema/src/utils/utils.service.spec.ts b/services/credential-schema/src/utils/utils.service.spec.ts
index e97bb6069..a17cd59f4 100644
--- a/services/credential-schema/src/utils/utils.service.spec.ts
+++ b/services/credential-schema/src/utils/utils.service.spec.ts
@@ -1,20 +1,84 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UtilsService } from './utils.service';
-import { HttpModule } from '@nestjs/axios';
+import { HttpService } from '@nestjs/axios';
+import { HttpException, InternalServerErrorException, Logger } from '@nestjs/common';
+import { of, throwError } from 'rxjs';
describe('UtilsService', () => {
- let service: UtilsService;
+ let utilsService: UtilsService;
+ let httpService: HttpService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
- imports: [HttpModule],
- providers: [UtilsService],
+ providers: [
+ UtilsService,
+ {
+ provide: HttpService,
+ useValue: {
+ axiosRef: {
+ post: jest.fn(),
+ },
+ },
+ },
+ ],
}).compile();
- service = module.get(UtilsService);
+ utilsService = module.get(UtilsService);
+ httpService = module.get(HttpService);
});
it('should be defined', () => {
- expect(service).toBeDefined();
+ expect(utilsService).toBeDefined();
+ });
+
+ describe('sign', () => {
+ it('should return signed VC response data', async () => {
+ const did = 'did:example:123';
+ const body = { some: 'payload' };
+ const signedVCResponse = { data: { signed: 'data' } };
+ (httpService.axiosRef.post as jest.Mock).mockResolvedValue(signedVCResponse);
+
+ const result = await utilsService.sign(did, body);
+ expect(result).toEqual(signedVCResponse.data);
+ expect(httpService.axiosRef.post).toHaveBeenCalledWith(
+ `${process.env.IDENTITY_BASE_URL}/utils/sign`,
+ { DID: did, payload: body }
+ );
+ });
+
+ it('should throw HttpException when request fails', async () => {
+ const did = 'did:example:123';
+ const body = { some: 'payload' };
+ const error = new Error('Request failed');
+ (error as any).response = { data: 'Error data' };
+ (httpService.axiosRef.post as jest.Mock).mockRejectedValue(error);
+
+ await expect(utilsService.sign(did, body)).rejects.toThrow(HttpException);
+ await expect(utilsService.sign(did, body)).rejects.toThrow("Couldn't sign the schema");
+ });
+ });
+
+ describe('generateDID', () => {
+ it('should return generated DID', async () => {
+ const body = { some: 'payload' };
+ const didResponse = { data: ['did:example:123'] };
+ (httpService.axiosRef.post as jest.Mock).mockResolvedValue(didResponse);
+
+ const result = await utilsService.generateDID(body);
+ expect(result).toEqual(didResponse.data[0]);
+ expect(httpService.axiosRef.post).toHaveBeenCalledWith(
+ `${process.env.IDENTITY_BASE_URL}/did/generate`,
+ body
+ );
+ });
+
+ it('should throw InternalServerErrorException when request fails', async () => {
+ const body = { some: 'payload' };
+ const error = new Error('Request failed');
+ (httpService.axiosRef.post as jest.Mock).mockRejectedValue(error);
+
+ await expect(utilsService.generateDID(body)).rejects.toThrow(InternalServerErrorException);
+ await expect(utilsService.generateDID(body)).rejects.toThrow('Can not generate a new DID');
+ });
});
});
diff --git a/services/credential-schema/test/app.e2e-spec.ts b/services/credential-schema/test/app.e2e-spec.ts
index 5cc230c04..633dad737 100644
--- a/services/credential-schema/test/app.e2e-spec.ts
+++ b/services/credential-schema/test/app.e2e-spec.ts
@@ -206,7 +206,40 @@ describe('AppController (e2e)', () => {
expect(res.schema.version).toBe('1.0.0');
});
- it('should create a schema, publish it, update it, deprecate it', () => {
- return;
+ it('should create a schema, publish it, update it, deprecate it', async () => {
+ const schemaPayload = generateCredentialSchemaTestBody();
+ const did = await utilsService.generateDID(generateTestDIDBody());
+ schemaPayload.schema.author = did.id;
+ const { body: schema } = await request(httpServer)
+ .post('/credential-schema')
+ .send(schemaPayload)
+ .expect(201);
+ const { body: resPublish } = await request(httpServer)
+ .put(
+ `/credential-schema/publish/${schema.schema.id}/${schema.schema.version}`,
+ )
+ .expect(200);
+ expect(resPublish.status).toBe('PUBLISHED');
+ expect(resPublish.schema.version).toBe('1.0.0');
+ const newSchemaPayload = generateCredentialSchemaTestBody();
+ newSchemaPayload.schema.id = resPublish.schema.id;
+ newSchemaPayload.schema.author = resPublish.schema.author;
+ const { body: newSchema } = await request(httpServer)
+ .put(
+ `/credential-schema/${resPublish.schema.id}/${resPublish.schema.version}`,
+ )
+ .send(newSchemaPayload)
+ .expect(200);
+ expect(newSchema.schema.id).toBe(resPublish.schema.id);
+ expect(newSchema.schema.version).toEqual('2.0.0');
+ expect(newSchema.schema.author).toEqual(resPublish.schema.author);
+ expect(newSchema.status).toEqual('DRAFT');
+ const { body: res } = await request(httpServer)
+ .put(
+ `/credential-schema/deprecate/${newSchema.schema.id}/${newSchema.schema.version}`,
+ )
+ .expect(200);
+ expect(res.status).toBe('DEPRECATED');
+ expect(res.schema.version).toBe('2.0.0');
});
});
diff --git a/services/credentials-service/docker-compose-test.yml b/services/credentials-service/docker-compose-test.yml
index 3fff51809..272f1c64f 100644
--- a/services/credentials-service/docker-compose-test.yml
+++ b/services/credentials-service/docker-compose-test.yml
@@ -1,4 +1,4 @@
-version: '3'
+version: '2.4'
services:
db-test:
diff --git a/services/credentials-service/docker-compose.yml b/services/credentials-service/docker-compose.yml
index 4e0c0aeb9..06802a63a 100644
--- a/services/credentials-service/docker-compose.yml
+++ b/services/credentials-service/docker-compose.yml
@@ -1,4 +1,4 @@
-version: '3'
+version: '2.4'
services:
db:
diff --git a/services/credentials-service/package.json b/services/credentials-service/package.json
index e46c8b6bc..8b44e44a4 100644
--- a/services/credentials-service/package.json
+++ b/services/credentials-service/package.json
@@ -15,7 +15,7 @@
"start:prod": "node dist/main",
"start:migrate:prod": "npx prisma migrate deploy && node dist/src/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
- "test:migrate": "npx prisma migrate deploy && jest --coverage && jest --config ./test/jest-e2e.json",
+ "test:migrate": "npx prisma migrate deploy && node --experimental-vm-modules node_modules/.bin/jest --coverage",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
@@ -27,24 +27,25 @@
"@digitalbazaar/ed25519-signature-2020": "^5.2.0",
"@digitalbazaar/ed25519-verification-key-2018": "^4.0.0",
"@digitalbazaar/ed25519-verification-key-2020": "^4.1.0",
- "@digitalbazaar/vc": "^6.2.0",
- "@fastify/static": "^6.6.0",
- "@fastify/view": "^7.4.1",
- "@nestjs/axios": "^0.1.2",
- "@nestjs/common": "^8.0.0",
+ "@digitalbazaar/vc": "^6.3.0",
+ "@fastify/static": "^7.0.4",
+ "@fastify/view": "^9.1.0",
+ "@nestjs/axios": "^3.0.2",
+ "@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.2.0",
- "@nestjs/core": "^8.0.0",
- "@nestjs/platform-express": "^8.0.0",
+ "@nestjs/core": "^9.0.0",
+ "@nestjs/platform-express": "^9.0.0",
"@nestjs/platform-fastify": "^9.2.1",
"@nestjs/swagger": "^6.1.4",
- "@nestjs/terminus": "^9.1.4",
+ "@nestjs/terminus": "^10.0.1",
"@prisma/client": "4.8.1",
"@techsavvyash/bitstring": "^3.2.0",
"@types/uuid": "^9.0.1",
"ajv": "^8.12.0",
+ "axios": "^1.7.2",
"crypto-ld": "3.9.0",
"did-jwt-vc": "^3.1.0",
- "fastify-helmet": "^7.1.0",
+ "@fastify/helmet": "8.0.0",
"fastify-multer": "^2.0.3",
"handlebars": "^4.7.7",
"jsonld": "^8.3.2",
@@ -52,7 +53,7 @@
"jsonwebtoken": "^8.5.1",
"jszip": "^3.10.1",
"prisma": "4.8.1",
- "qrcode": "^1.5.1",
+ "qrcode": "^1.5.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
@@ -61,27 +62,27 @@
"zod": "^3.21.4"
},
"devDependencies": {
- "@nestjs/cli": "^8.0.0",
- "@nestjs/schematics": "^8.0.0",
- "@nestjs/testing": "^8.0.0",
+ "@nestjs/cli": "^9.0.0",
+ "@nestjs/schematics": "^9.0.0",
+ "@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
- "@types/jest": "27.0.2",
- "@types/node": "^16.0.0",
+ "@types/jest": "^29.5.12",
+ "@types/node": "^20.14.2",
+ "@types/qrcode": "^1.5.5",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
- "jest": "^29.2.5",
+ "jest": "^29.7.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
- "supertest": "^6.1.3",
- "ts-jest": "^29.0.5",
- "ts-loader": "^9.2.3",
+ "supertest": "7.0.0",
+ "ts-jest": "^29.1.2",
"ts-node": "^10.0.0",
- "tsconfig-paths": "^3.10.1",
- "typescript": "^4.3.5"
+ "tsconfig-paths": "4.1.0",
+ "typescript": "4.9.5"
},
"jest": {
"moduleFileExtensions": [
@@ -95,9 +96,14 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
- "**/*.(t|j)s"
+ "**/*.(t|j)s",
+ "!**/*.module.ts",
+ "!**/main.ts"
],
"coverageDirectory": "../coverage",
- "testEnvironment": "node"
+ "testEnvironment": "node",
+ "moduleNameMapper": {
+ "^src/(.*)$": "/$1"
+ }
}
}
diff --git a/services/credentials-service/src/app.controller.spec.ts b/services/credentials-service/src/app.controller.spec.ts
index 8e7220896..3480f23e6 100644
--- a/services/credentials-service/src/app.controller.spec.ts
+++ b/services/credentials-service/src/app.controller.spec.ts
@@ -1,19 +1,36 @@
-import { HttpModule } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaClient } from '@prisma/client';
-import { TerminusModule } from '@nestjs/terminus';
import { HealthCheckUtilsService } from './credentials/utils/healthcheck.utils.service';
+import { HealthCheckError, HealthCheckService, TerminusModule } from '@nestjs/terminus';
+import { HttpModule, HttpService } from '@nestjs/axios';
+import { AXIOS_INSTANCE_TOKEN } from '@nestjs/axios/dist/http.constants';
+import axios from 'axios';
+import { HealthCheckExecutor } from '@nestjs/terminus/dist/health-check/health-check-executor.service';
+import { getErrorLoggerProvider } from '@nestjs/terminus/dist/health-check/error-logger/error-logger.provider';
+import { getLoggerProvider } from '@nestjs/terminus/dist/health-check/logger/logger.provider';
+
+const mockAxiosInstance = axios.create(); // Create a mock Axios instance
+
+const mockHttpService = {
+ provide: AXIOS_INSTANCE_TOKEN,
+ useValue: mockAxiosInstance,
+};
describe('AppController', () => {
let appController: AppController;
- beforeEach(async () => {
+ beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
- imports: [HttpModule, TerminusModule],
+ imports: [TerminusModule, HttpModule],
controllers: [AppController],
- providers: [AppService, PrismaClient, HealthCheckUtilsService],
+ providers: [
+ getLoggerProvider(),
+ getErrorLoggerProvider(),
+ HealthCheckExecutor,
+ HealthCheckError,
+ mockHttpService, HealthCheckService, AppService, PrismaClient, HealthCheckUtilsService],
}).compile();
appController = app.get(AppController);
diff --git a/services/credentials-service/src/app.module.ts b/services/credentials-service/src/app.module.ts
index 87e332efe..13e59c4fa 100644
--- a/services/credentials-service/src/app.module.ts
+++ b/services/credentials-service/src/app.module.ts
@@ -1,10 +1,10 @@
-import { HttpModule } from '@nestjs/axios';
+import { HttpModule, HttpService } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CredentialsModule } from './credentials/credentials.module';
-import { TerminusModule } from '@nestjs/terminus';
+import { HealthCheckService, TerminusModule } from '@nestjs/terminus';
import { HealthCheckUtilsService } from './credentials/utils/healthcheck.utils.service';
import { PrismaClient } from '@prisma/client';
import { RevocationList } from './revocation-list/revocation-list.helper';
@@ -18,9 +18,9 @@ import { RevocationListModule } from './revocation-list/revocation-list.module';
ConfigModule.forRoot({ isGlobal: true }),
CredentialsModule,
TerminusModule,
- RevocationListModule
+ RevocationListModule,
],
controllers: [AppController],
- providers: [AppService, ConfigService, PrismaClient, HealthCheckUtilsService, RevocationList, RevocationListImpl, RevocationListService],
+ providers: [HttpService, HealthCheckService, AppService, ConfigService, PrismaClient, HealthCheckUtilsService, RevocationList, RevocationListImpl, RevocationListService],
})
export class AppModule {}
diff --git a/services/credentials-service/src/credentials/credentials.controller.spec.ts b/services/credentials-service/src/credentials/credentials.controller.spec.ts
index 1e03b44c6..b0ac2e3fd 100644
--- a/services/credentials-service/src/credentials/credentials.controller.spec.ts
+++ b/services/credentials-service/src/credentials/credentials.controller.spec.ts
@@ -1,32 +1,192 @@
-import { HttpModule } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
import { CredentialsController } from './credentials.controller';
import { CredentialsService } from './credentials.service';
-import { IdentityUtilsService } from './utils/identity.utils.service';
-import { SchemaUtilsSerivce } from './utils/schema.utils.service';
-import { RenderingUtilsService } from './utils/rendering.utils.service';
-import { PrismaClient } from '@prisma/client';
+import { GetCredentialsBySubjectOrIssuer } from './dto/getCredentialsBySubjectOrIssuer.dto';
+import { IssueCredentialDTO } from './dto/issue-credential.dto';
+import { VerifyCredentialDTO } from './dto/verify-credential.dto';
+import { Request } from 'express';
+import { RENDER_OUTPUT } from './enums/renderOutput.enum';
+import { BadRequestException } from '@nestjs/common';
+import { string, undefined } from 'zod';
+import { HttpModule } from '@nestjs/axios';
describe('CredentialsController', () => {
let controller: CredentialsController;
+ let service: CredentialsService;
- beforeEach(async () => {
+ beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule],
controllers: [CredentialsController],
providers: [
- CredentialsService,
- PrismaClient,
- IdentityUtilsService,
- SchemaUtilsSerivce,
- RenderingUtilsService,
+ {
+ provide: CredentialsService,
+ useValue: {
+ getCredentials: jest.fn(),
+ getCredentialsBySubjectOrIssuer: jest.fn(),
+ getCredentialById: jest.fn(),
+ issueCredential: jest.fn(),
+ deleteCredential: jest.fn(),
+ verifyCredentialById: jest.fn(),
+ verifyCredential: jest.fn(),
+ getRevocationList: jest.fn(),
+ },
+ },
],
}).compile();
controller = module.get(CredentialsController);
+ service = module.get(CredentialsService);
});
+ beforeEach(async () => {
+ jest.restoreAllMocks();
+ })
+
it('should be defined', () => {
expect(controller).toBeDefined();
});
+
+ describe('getCredentials', () => {
+ it('should return credentials based on tags, page, and limit', async () => {
+ const tags = 'tag1,tag2';
+ const page = '1';
+ const limit = '10';
+ const expectedResult = [];
+
+ jest.spyOn(service, 'getCredentials').mockResolvedValue(expectedResult);
+
+ const result = await controller.getCredentials(tags, page, limit);
+ expect(result).toBe(expectedResult);
+ expect(service.getCredentials).toHaveBeenCalledWith(
+ ['tag1', 'tag2'],
+ 1,
+ 10
+ );
+ });
+ });
+
+ describe('getCredentialsBySubject', () => {
+ it('should return credentials based on subject or issuer', async () => {
+ const getCreds: GetCredentialsBySubjectOrIssuer = { subject: { id: 'subject' } };
+ const page = '1';
+ const limit = '10';
+ const expectedResult = [];
+
+ jest.spyOn(service, 'getCredentialsBySubjectOrIssuer').mockResolvedValue(expectedResult);
+
+ const result = await controller.getCredentialsBySubject(getCreds, page, limit);
+ expect(result).toBe(expectedResult);
+ expect(service.getCredentialsBySubjectOrIssuer).toHaveBeenCalledWith(
+ getCreds,
+ 1,
+ 10
+ );
+ });
+ });
+
+ describe('getCredentialById', () => {
+ it('should return a credential by id', async () => {
+ const id = '1';
+ const req = { headers: { accept: 'application/json' } } as Request;
+ const template = 'abc';
+ const requestedTemplateId: string = req.headers['templateid'] as string;
+ req.headers['template'] = template;
+ const expectedResult = {};
+
+ jest.spyOn(service, 'getCredentialById').mockResolvedValue(expectedResult);
+
+ const result = await controller.getCredentialById(id, req);
+ expect(result).toBe(expectedResult);
+ expect(service.getCredentialById).toHaveBeenCalledWith(
+ id,
+ requestedTemplateId,
+ template,
+ RENDER_OUTPUT.JSON
+ );
+ });
+
+ it('should throw BadRequestException when template id is missing and accept is not JSON', async () => {
+ const id = '1';
+ const req = { headers: { accept: 'application/pdf' } } as Request;
+
+ await expect(async () => controller.getCredentialById(id, req)).rejects.toThrow(new BadRequestException("Template id is required"));
+ });
+ });
+
+ describe('issueCredentials', () => {
+ it('should issue credentials', async () => {
+ const issueRequest: IssueCredentialDTO = {
+ credential: undefined,
+ credentialSchemaId: '',
+ credentialSchemaVersion: '',
+ tags: [] /* ... */ };
+ const expectedResult = {};
+
+ jest.spyOn(service, 'issueCredential').mockResolvedValue(expectedResult as any);
+
+ const result = await controller.issueCredentials(issueRequest);
+ expect(result).toBe(expectedResult);
+ expect(service.issueCredential).toHaveBeenCalledWith(issueRequest);
+ });
+ });
+
+ describe('deleteCredential', () => {
+ it('should delete a credential', async () => {
+ const id = '1';
+ const expectedResult = {};
+
+ jest.spyOn(service, 'deleteCredential').mockResolvedValue(expectedResult as any);
+
+ const result = await controller.deleteCredential(id);
+ expect(result).toBe(expectedResult);
+ expect(service.deleteCredential).toHaveBeenCalledWith(id);
+ });
+ });
+
+ describe('verifyCredentialById', () => {
+ it('should verify a credential by id', async () => {
+ const id = '1';
+ const expectedResult = {};
+
+ jest.spyOn(service, 'verifyCredentialById').mockResolvedValue(expectedResult as any);
+
+ const result = await controller.verifyCredentialById(id);
+ expect(result).toBe(expectedResult);
+ expect(service.verifyCredentialById).toHaveBeenCalledWith(id);
+ });
+ });
+
+ describe('verifyCredential', () => {
+ it('should verify a credential',
+ async () => {
+ const verifyRequest: VerifyCredentialDTO = { verifiableCredential: {} } as any;
+ const expectedResult = {};
+
+ jest.spyOn(service, 'verifyCredential').mockResolvedValue(expectedResult as any);
+
+ const result = await controller.verifyCredential(verifyRequest);
+ expect(result).toBe(expectedResult);
+ expect(service.verifyCredential).toHaveBeenCalledWith(verifyRequest.verifiableCredential);
+ });
+ });
+
+ describe('getRevocationList', () => {
+ it('should return a list of revoked credentials', async () => {
+ const issuerId = 'issuer';
+ const page = '1';
+ const limit = '1000';
+ const expectedResult = [];
+
+ jest.spyOn(service, 'getRevocationList').mockResolvedValue(expectedResult);
+
+ const result = await controller.getRevocationList(issuerId, page, limit);
+ expect(result).toBe(expectedResult);
+ expect(service.getRevocationList).toHaveBeenCalledWith(
+ issuerId,
+ 1,
+ 1000
+ );
+ });
+ });
});
diff --git a/services/credentials-service/src/credentials/credentials.controller.ts b/services/credentials-service/src/credentials/credentials.controller.ts
index 5c2b43549..f2525b4a5 100644
--- a/services/credentials-service/src/credentials/credentials.controller.ts
+++ b/services/credentials-service/src/credentials/credentials.controller.ts
@@ -111,7 +111,7 @@ export class CredentialsController {
}
@Delete(':id')
- delteCredential(@Param('id') id: string) {
+ deleteCredential(@Param('id') id: string) {
return this.credentialsService.deleteCredential(id);
}
diff --git a/services/credentials-service/src/credentials/credentials.fixtures.ts b/services/credentials-service/src/credentials/credentials.fixtures.ts
index 8a0542994..4bfca84e2 100644
--- a/services/credentials-service/src/credentials/credentials.fixtures.ts
+++ b/services/credentials-service/src/credentials/credentials.fixtures.ts
@@ -76,7 +76,22 @@ export const generateCredentialRequestPayload = (
credential: {
'@context': [
'https://www.w3.org/2018/credentials/v1',
- 'https://www.w3.org/2018/credentials/examples/v1',
+ {
+ "schema": "https://schema.org/",
+ "UniversityDegreeCredential": {
+ "@id": "urn:UniversityDegreeCredential",
+ "@context": {
+ "@version": 1.1,
+ "@protected": true,
+ "id": "@id",
+ "type": "@type",
+ "grade": "schema:grade",
+ "programme": "schema:programme",
+ "certifyingInstitute": "schema:certifyingInstitute",
+ "evaluatingInstitute": "schema:evaluatingInstitute"
+ }
+ }
+ }
],
type: ['VerifiableCredential', 'UniversityDegreeCredential'],
issuer: issuerid,
@@ -84,6 +99,7 @@ export const generateCredentialRequestPayload = (
expirationDate: '2023-02-08T11:56:27.259Z',
credentialSubject: {
id: subjectid,
+ type: "UniversityDegreeCredential",
grade: '9.23',
programme: 'B.Tech',
certifyingInstitute: 'IIIT Sonepat',
@@ -156,8 +172,8 @@ export const issueCredentialReturnTypeSchema = {
],
},
credentialSchemaId: { type: 'string' },
- createdAt: { type: 'object', format: 'custom-date-time' },
- updatedAt: { type: 'object', format: 'custom-date-time' },
+ createdAt: { type: 'string', format: 'date-time' },
+ updatedAt: { type: 'string', format: 'date-time' },
createdBy: { type: 'string' },
updatedBy: { type: 'string' },
tags: {
diff --git a/services/credentials-service/src/credentials/credentials.module.ts b/services/credentials-service/src/credentials/credentials.module.ts
index 43530bd68..9c34c0912 100644
--- a/services/credentials-service/src/credentials/credentials.module.ts
+++ b/services/credentials-service/src/credentials/credentials.module.ts
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { CredentialsService } from './credentials.service';
import { CredentialsController } from './credentials.controller';
-import { HttpModule } from '@nestjs/axios';
+import { HttpModule, HttpService } from '@nestjs/axios';
import { IdentityUtilsService } from './utils/identity.utils.service';
import { RenderingUtilsService } from './utils/rendering.utils.service';
import { SchemaUtilsSerivce } from './utils/schema.utils.service';
@@ -9,7 +9,7 @@ import { PrismaClient } from '@prisma/client';
@Module({
imports: [HttpModule],
- providers: [CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce],
+ providers: [HttpService, CredentialsService, PrismaClient, IdentityUtilsService, RenderingUtilsService, SchemaUtilsSerivce],
controllers: [CredentialsController],
exports: [IdentityUtilsService]
})
diff --git a/services/credentials-service/src/credentials/credentials.service.spec.ts b/services/credentials-service/src/credentials/credentials.service.spec.ts
index d07ad3704..ca94ff28e 100644
--- a/services/credentials-service/src/credentials/credentials.service.spec.ts
+++ b/services/credentials-service/src/credentials/credentials.service.spec.ts
@@ -1,8 +1,7 @@
-import { HttpModule, HttpService } from '@nestjs/axios';
import { Test, TestingModule } from '@nestjs/testing';
import { CredentialsService } from './credentials.service';
import Ajv2019 from 'ajv/dist/2019';
-import { UnsignedVCValidator, VCValidator } from './types/validators/index';
+import { UnsignedVCValidator, VCValidator } from './types/validators';
import { SchemaUtilsSerivce } from './utils/schema.utils.service';
import { IdentityUtilsService } from './utils/identity.utils.service';
import { RenderingUtilsService } from './utils/rendering.utils.service';
@@ -15,12 +14,25 @@ import {
generateRenderingTemplatePayload,
} from './credentials.fixtures';
import { RENDER_OUTPUT } from './enums/renderOutput.enum';
+import { TerminusModule } from '@nestjs/terminus';
+import { HttpModule, HttpService } from '@nestjs/axios';
// setup ajv
const ajv = new Ajv2019({ strictTuples: false });
-ajv.addFormat('custom-date-time', function (dateTimeString) {
- return typeof dateTimeString === typeof new Date();
-});
+ajv.addFormat('date-time', function isValidDateTime(dateTimeString) {
+ // Regular expression for ISO 8601 date-time format
+ const iso8601Regex = /^(\d{4}-[01]\d-[0-3]\d[T\s](?:[0-2]\d:[0-5]\d:[0-5]\d(?:\.\d+)?|23:59:60)(?:Z|[+-][0-2]\d:[0-5]\d)?)$/;
+
+ // Check if the string matches the ISO 8601 format
+ if (!iso8601Regex.test(dateTimeString)) {
+ return false;
+ }
+
+ // Check if the string can be parsed into a valid date
+ const date = new Date(dateTimeString);
+ return !isNaN(date.getTime());
+ }
+);
describe('CredentialsService', () => {
let service: CredentialsService;
@@ -35,9 +47,9 @@ describe('CredentialsService', () => {
let credentialSchemaID;
let sampleCredReqPayload;
- beforeEach(async () => {
+ beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
- imports: [HttpModule],
+ imports: [TerminusModule, HttpModule],
providers: [
CredentialsService,
PrismaClient,
@@ -77,6 +89,10 @@ describe('CredentialsService', () => {
);
});
+ beforeEach(async () => {
+ jest.restoreAllMocks();
+ })
+
it('service should be defined', () => {
expect(service).toBeDefined();
});
@@ -87,59 +103,62 @@ describe('CredentialsService', () => {
expect(validate(newCred)).toBe(true);
});
- it('should get a credential in JSON', async () => {
- const newCred: any = await service.issueCredential(sampleCredReqPayload);
- const cred = await service.getCredentialById(newCred.credential?.id);
- UnsignedVCValidator.parse(cred);
- expect(getCredReqValidate(cred)).toBe(true);
- });
-
- it('should get a credential in QR', async () => {
- const newCred: any = await service.issueCredential(sampleCredReqPayload);
- const dataURL = await service.getCredentialById(newCred.credential?.id, undefined, RENDER_OUTPUT.QR);
- expect(dataURL).toBeDefined(); // Assert that the dataURL is defined
- expect(dataURL).toContain('data:image/png;base64,');
- });
-
-
- it('should get a credential in HTML', async () => {
- const newCred: any = await service.issueCredential(sampleCredReqPayload);
- const templatePayload = generateRenderingTemplatePayload(newCred.credentialSchemaId, "1.0.0")
- const template = await httpSerivce.axiosRef.post(`${process.env.SCHEMA_BASE_URL}/template`, templatePayload);
- const cred = await service.getCredentialById(newCred.credential?.id, template.data.template.templateId, RENDER_OUTPUT.HTML);
- expect(cred).toContain('