diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 764cb00..cb37ece 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -844,218 +844,6 @@ const docTemplate = `{ } } }, - "/obligation_maps/license/{id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [], - "{}": [] - } - ], - "description": "Get obligation maps for a given license id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Get maps for a license", - "operationId": "GetObligationMapByLicenseId", - "parameters": [ - { - "type": "string", - "description": "id of the license", - "name": "license", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "404": { - "description": "No license with given id found", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, - "/obligation_maps/obligation/{id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [], - "{}": [] - } - ], - "description": "Get obligation maps for a given obligation id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Get maps for an obligation", - "operationId": "GetObligationMapByObligationId", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "404": { - "description": "No obligation with given id found", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, - "/obligation_maps/obligation/{id}/license": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Replaces the license list of an obligation id with the given list in the obligation map.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Change license list", - "operationId": "UpdateLicenseInObligationMap", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Ids of the licenses to be in map", - "name": "Ids", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LicenseListInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "400": { - "description": "Invalid json body", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "404": { - "description": "No license or obligation found.", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - }, - "patch": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add or remove licenses from obligation map for a given obligation id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Add or remove licenses from obligation map", - "operationId": "PatchObligationMap", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "License ids with action", - "name": "license_maps", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LicenseMapInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "400": { - "description": "Invalid json body", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "404": { - "description": "No license or obligation found.", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "500": { - "description": "Failure to insert new maps", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, "/obligations": { "get": { "security": [ @@ -2490,12 +2278,6 @@ const docTemplate = `{ } }, "definitions": { - "datatypes.JSONType-models_LicenseDBSchemaExtension": { - "type": "object" - }, - "datatypes.JSONType-models_ObligationSchemaExtension": { - "type": "object" - }, "models.APICollection": { "type": "object", "properties": { @@ -2746,7 +2528,7 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -2788,68 +2570,6 @@ const docTemplate = `{ } } }, - "models.LicenseDB": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "addDate": { - "type": "string" - }, - "copyleft": { - "type": "boolean" - }, - "externalRef": { - "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" - }, - "fullname": { - "type": "string" - }, - "id": { - "type": "string" - }, - "notes": { - "type": "string" - }, - "obligations": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Obligation" - } - }, - "osiapproved": { - "type": "boolean" - }, - "risk": { - "type": "integer" - }, - "shortname": { - "type": "string" - }, - "source": { - "type": "string" - }, - "spdxId": { - "type": "string" - }, - "text": { - "type": "string" - }, - "textUpdatable": { - "type": "boolean" - }, - "url": { - "type": "string" - }, - "user": { - "$ref": "#/definitions/models.User" - }, - "userId": { - "type": "string" - } - } - }, "models.LicenseDBSchemaExtension": { "type": "object", "properties": { @@ -2903,41 +2623,6 @@ const docTemplate = `{ } } }, - "models.LicenseListInput": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "object" - } - } - } - }, - "models.LicenseMapElement": { - "type": "object", - "properties": { - "add": { - "type": "boolean", - "example": true - }, - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - } - } - }, - "models.LicenseMapInput": { - "type": "object", - "properties": { - "map": { - "type": "array", - "items": { - "$ref": "#/definitions/models.LicenseMapElement" - } - } - } - }, "models.LicensePreview": { "type": "object", "properties": { @@ -3017,7 +2702,7 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -3078,10 +2763,10 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { - "$ref": "#/definitions/models.Obligation" + "type": "string" } }, "risk": { @@ -3116,62 +2801,6 @@ const docTemplate = `{ } } }, - "models.Obligation": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "category": { - "type": "string", - "enum": [ - "DISTRIBUTION", - "PATENT", - "INTERNAL", - "CONTRACTUAL", - "EXPORT_CONTROL", - "GENERAL" - ], - "example": "DISTRIBUTION" - }, - "classification": { - "$ref": "#/definitions/models.ObligationClassification" - }, - "comment": { - "type": "string" - }, - "externalRef": { - "$ref": "#/definitions/datatypes.JSONType-models_ObligationSchemaExtension" - }, - "id": { - "type": "string" - }, - "licenses": { - "type": "array", - "items": { - "$ref": "#/definitions/models.LicenseDB" - } - }, - "obligationClassificationId": { - "type": "string" - }, - "obligationTypeId": { - "type": "string" - }, - "text": { - "type": "string" - }, - "textUpdatable": { - "type": "boolean" - }, - "topic": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/models.ObligationType" - } - } - }, "models.ObligationClassification": { "type": "object", "required": [ @@ -3290,66 +2919,6 @@ const docTemplate = `{ } } }, - "models.ObligationMapLicenseFormat": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - }, - "shortname": { - "type": "string", - "example": "MIT" - } - } - }, - "models.ObligationMapResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ObligationMapUser" - } - }, - "paginationmeta": { - "$ref": "#/definitions/models.PaginationMeta" - }, - "status": { - "type": "integer", - "example": 200 - } - } - }, - "models.ObligationMapUser": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - }, - "licenses": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ObligationMapLicenseFormat" - } - }, - "topic": { - "type": "string", - "example": "copyleft" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ], - "example": "obligation" - } - } - }, "models.ObligationPreview": { "type": "object", "properties": { diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 9894923..2228243 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -837,218 +837,6 @@ } } }, - "/obligation_maps/license/{id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [], - "{}": [] - } - ], - "description": "Get obligation maps for a given license id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Get maps for a license", - "operationId": "GetObligationMapByLicenseId", - "parameters": [ - { - "type": "string", - "description": "id of the license", - "name": "license", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "404": { - "description": "No license with given id found", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, - "/obligation_maps/obligation/{id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [], - "{}": [] - } - ], - "description": "Get obligation maps for a given obligation id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Get maps for an obligation", - "operationId": "GetObligationMapByObligationId", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "404": { - "description": "No obligation with given id found", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, - "/obligation_maps/obligation/{id}/license": { - "put": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Replaces the license list of an obligation id with the given list in the obligation map.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Change license list", - "operationId": "UpdateLicenseInObligationMap", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Ids of the licenses to be in map", - "name": "Ids", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LicenseListInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "400": { - "description": "Invalid json body", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "404": { - "description": "No license or obligation found.", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - }, - "patch": { - "security": [ - { - "ApiKeyAuth": [] - } - ], - "description": "Add or remove licenses from obligation map for a given obligation id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Obligations" - ], - "summary": "Add or remove licenses from obligation map", - "operationId": "PatchObligationMap", - "parameters": [ - { - "type": "string", - "description": "Id of the obligation", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "License ids with action", - "name": "license_maps", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.LicenseMapInput" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.ObligationMapResponse" - } - }, - "400": { - "description": "Invalid json body", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "404": { - "description": "No license or obligation found.", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - }, - "500": { - "description": "Failure to insert new maps", - "schema": { - "$ref": "#/definitions/models.LicenseError" - } - } - } - } - }, "/obligations": { "get": { "security": [ @@ -2483,12 +2271,6 @@ } }, "definitions": { - "datatypes.JSONType-models_LicenseDBSchemaExtension": { - "type": "object" - }, - "datatypes.JSONType-models_ObligationSchemaExtension": { - "type": "object" - }, "models.APICollection": { "type": "object", "properties": { @@ -2739,7 +2521,7 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -2781,68 +2563,6 @@ } } }, - "models.LicenseDB": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "addDate": { - "type": "string" - }, - "copyleft": { - "type": "boolean" - }, - "externalRef": { - "$ref": "#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension" - }, - "fullname": { - "type": "string" - }, - "id": { - "type": "string" - }, - "notes": { - "type": "string" - }, - "obligations": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Obligation" - } - }, - "osiapproved": { - "type": "boolean" - }, - "risk": { - "type": "integer" - }, - "shortname": { - "type": "string" - }, - "source": { - "type": "string" - }, - "spdxId": { - "type": "string" - }, - "text": { - "type": "string" - }, - "textUpdatable": { - "type": "boolean" - }, - "url": { - "type": "string" - }, - "user": { - "$ref": "#/definitions/models.User" - }, - "userId": { - "type": "string" - } - } - }, "models.LicenseDBSchemaExtension": { "type": "object", "properties": { @@ -2896,41 +2616,6 @@ } } }, - "models.LicenseListInput": { - "type": "object", - "properties": { - "ids": { - "type": "array", - "items": { - "type": "object" - } - } - } - }, - "models.LicenseMapElement": { - "type": "object", - "properties": { - "add": { - "type": "boolean", - "example": true - }, - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - } - } - }, - "models.LicenseMapInput": { - "type": "object", - "properties": { - "map": { - "type": "array", - "items": { - "$ref": "#/definitions/models.LicenseMapElement" - } - } - } - }, "models.LicensePreview": { "type": "object", "properties": { @@ -3010,7 +2695,7 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -3071,10 +2756,10 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { - "$ref": "#/definitions/models.Obligation" + "type": "string" } }, "risk": { @@ -3109,62 +2794,6 @@ } } }, - "models.Obligation": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - }, - "category": { - "type": "string", - "enum": [ - "DISTRIBUTION", - "PATENT", - "INTERNAL", - "CONTRACTUAL", - "EXPORT_CONTROL", - "GENERAL" - ], - "example": "DISTRIBUTION" - }, - "classification": { - "$ref": "#/definitions/models.ObligationClassification" - }, - "comment": { - "type": "string" - }, - "externalRef": { - "$ref": "#/definitions/datatypes.JSONType-models_ObligationSchemaExtension" - }, - "id": { - "type": "string" - }, - "licenses": { - "type": "array", - "items": { - "$ref": "#/definitions/models.LicenseDB" - } - }, - "obligationClassificationId": { - "type": "string" - }, - "obligationTypeId": { - "type": "string" - }, - "text": { - "type": "string" - }, - "textUpdatable": { - "type": "boolean" - }, - "topic": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/models.ObligationType" - } - } - }, "models.ObligationClassification": { "type": "object", "required": [ @@ -3283,66 +2912,6 @@ } } }, - "models.ObligationMapLicenseFormat": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - }, - "shortname": { - "type": "string", - "example": "MIT" - } - } - }, - "models.ObligationMapResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ObligationMapUser" - } - }, - "paginationmeta": { - "$ref": "#/definitions/models.PaginationMeta" - }, - "status": { - "type": "integer", - "example": 200 - } - } - }, - "models.ObligationMapUser": { - "type": "object", - "properties": { - "id": { - "type": "string", - "example": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6" - }, - "licenses": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ObligationMapLicenseFormat" - } - }, - "topic": { - "type": "string", - "example": "copyleft" - }, - "type": { - "type": "string", - "enum": [ - "obligation", - "restriction", - "risk", - "right" - ], - "example": "obligation" - } - } - }, "models.ObligationPreview": { "type": "object", "properties": { diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 7928f6e..c931f08 100644 --- a/cmd/laas/docs/swagger.yaml +++ b/cmd/laas/docs/swagger.yaml @@ -1,9 +1,5 @@ basePath: /api/v1 definitions: - datatypes.JSONType-models_LicenseDBSchemaExtension: - type: object - datatypes.JSONType-models_ObligationSchemaExtension: - type: object models.APICollection: properties: authenticated: @@ -174,7 +170,7 @@ definitions: notes: example: This license has been superseded. type: string - obligations: + obligation_ids: example: - f81d4fae-7dec-11d0-a765-00a0c91e6bf6 - f812jfae-7dbc-11d0-a765-00a0hf06bf6 @@ -210,47 +206,6 @@ definitions: - spdx_id - text type: object - models.LicenseDB: - properties: - active: - type: boolean - addDate: - type: string - copyleft: - type: boolean - externalRef: - $ref: '#/definitions/datatypes.JSONType-models_LicenseDBSchemaExtension' - fullname: - type: string - id: - type: string - notes: - type: string - obligations: - items: - $ref: '#/definitions/models.Obligation' - type: array - osiapproved: - type: boolean - risk: - type: integer - shortname: - type: string - source: - type: string - spdxId: - type: string - text: - type: string - textUpdatable: - type: boolean - url: - type: string - user: - $ref: '#/definitions/models.User' - userId: - type: string - type: object models.LicenseDBSchemaExtension: properties: license_explanation: @@ -288,29 +243,6 @@ definitions: example: 200 type: integer type: object - models.LicenseListInput: - properties: - ids: - items: - type: object - type: array - type: object - models.LicenseMapElement: - properties: - add: - example: true - type: boolean - id: - example: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 - type: string - type: object - models.LicenseMapInput: - properties: - map: - items: - $ref: '#/definitions/models.LicenseMapElement' - type: array - type: object models.LicensePreview: properties: id: @@ -365,7 +297,7 @@ definitions: notes: example: This license has been superseded. type: string - obligations: + obligation_ids: items: type: string type: array @@ -409,9 +341,9 @@ definitions: notes: example: This license has been superseded. type: string - obligations: + obligation_ids: items: - $ref: '#/definitions/models.Obligation' + type: string type: array risk: example: 1 @@ -437,45 +369,6 @@ definitions: example: https://opensource.org/licenses/MIT type: string type: object - models.Obligation: - properties: - active: - type: boolean - category: - enum: - - DISTRIBUTION - - PATENT - - INTERNAL - - CONTRACTUAL - - EXPORT_CONTROL - - GENERAL - example: DISTRIBUTION - type: string - classification: - $ref: '#/definitions/models.ObligationClassification' - comment: - type: string - externalRef: - $ref: '#/definitions/datatypes.JSONType-models_ObligationSchemaExtension' - id: - type: string - licenses: - items: - $ref: '#/definitions/models.LicenseDB' - type: array - obligationClassificationId: - type: string - obligationTypeId: - type: string - text: - type: string - textUpdatable: - type: boolean - topic: - type: string - type: - $ref: '#/definitions/models.ObligationType' - type: object models.ObligationClassification: properties: classification: @@ -559,48 +452,6 @@ definitions: example: 200 type: integer type: object - models.ObligationMapLicenseFormat: - properties: - id: - example: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 - type: string - shortname: - example: MIT - type: string - type: object - models.ObligationMapResponse: - properties: - data: - items: - $ref: '#/definitions/models.ObligationMapUser' - type: array - paginationmeta: - $ref: '#/definitions/models.PaginationMeta' - status: - example: 200 - type: integer - type: object - models.ObligationMapUser: - properties: - id: - example: f81d4fae-7dec-11d0-a765-00a0c91e6bf6 - type: string - licenses: - items: - $ref: '#/definitions/models.ObligationMapLicenseFormat' - type: array - topic: - example: copyleft - type: string - type: - enum: - - obligation - - restriction - - risk - - right - example: obligation - type: string - type: object models.ObligationPreview: properties: id: @@ -1526,145 +1377,6 @@ paths: summary: Login tags: - Users - /obligation_maps/license/{id}: - get: - consumes: - - application/json - description: Get obligation maps for a given license id - operationId: GetObligationMapByLicenseId - parameters: - - description: id of the license - in: path - name: license - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.ObligationMapResponse' - "404": - description: No license with given id found - schema: - $ref: '#/definitions/models.LicenseError' - security: - - '{}': [] - ApiKeyAuth: [] - summary: Get maps for a license - tags: - - Obligations - /obligation_maps/obligation/{id}: - get: - consumes: - - application/json - description: Get obligation maps for a given obligation id - operationId: GetObligationMapByObligationId - parameters: - - description: Id of the obligation - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.ObligationMapResponse' - "404": - description: No obligation with given id found - schema: - $ref: '#/definitions/models.LicenseError' - security: - - '{}': [] - ApiKeyAuth: [] - summary: Get maps for an obligation - tags: - - Obligations - /obligation_maps/obligation/{id}/license: - patch: - consumes: - - application/json - description: Add or remove licenses from obligation map for a given obligation - id - operationId: PatchObligationMap - parameters: - - description: Id of the obligation - in: path - name: id - required: true - type: string - - description: License ids with action - in: body - name: license_maps - required: true - schema: - $ref: '#/definitions/models.LicenseMapInput' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.ObligationMapResponse' - "400": - description: Invalid json body - schema: - $ref: '#/definitions/models.LicenseError' - "404": - description: No license or obligation found. - schema: - $ref: '#/definitions/models.LicenseError' - "500": - description: Failure to insert new maps - schema: - $ref: '#/definitions/models.LicenseError' - security: - - ApiKeyAuth: [] - summary: Add or remove licenses from obligation map - tags: - - Obligations - put: - consumes: - - application/json - description: Replaces the license list of an obligation id with the given list - in the obligation map. - operationId: UpdateLicenseInObligationMap - parameters: - - description: Id of the obligation - in: path - name: id - required: true - type: string - - description: Ids of the licenses to be in map - in: body - name: Ids - required: true - schema: - $ref: '#/definitions/models.LicenseListInput' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.ObligationMapResponse' - "400": - description: Invalid json body - schema: - $ref: '#/definitions/models.LicenseError' - "404": - description: No license or obligation found. - schema: - $ref: '#/definitions/models.LicenseError' - security: - - ApiKeyAuth: [] - summary: Change license list - tags: - - Obligations /obligations: get: consumes: diff --git a/pkg/api/api.go b/pkg/api/api.go index 37e342a..92bd8c6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -156,13 +156,6 @@ func Router() *gin.Engine { obligations.DELETE("/classifications/:classification", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationClassification) obligations.POST("/similarity", getSimilarObligations) } - obMap := authorizedv1.Group("/obligation_maps") - { - obMap.GET("obligation/:id", GetObligationMapByObligationId) - obMap.GET("license/:id", GetObligationMapByLicenseId) - obMap.PATCH("obligation/:id/license", PatchObligationMap) - obMap.PUT("obligation/:id/license", UpdateLicenseInObligationMap) - } audit := authorizedv1.Group("/audits") { audit.GET("", GetAllAudit) @@ -205,11 +198,6 @@ func Router() *gin.Engine { obligations.GET("/types", GetAllObligationType) obligations.GET("/classifications", GetAllObligationClassification) } - obMap := unAuthorizedv1.Group("/obligation_maps") - { - obMap.GET("obligation/:id", GetObligationMapByObligationId) - obMap.GET("license/:id", GetObligationMapByLicenseId) - } audit := unAuthorizedv1.Group("/audits") { audit.GET("", GetAllAudit) @@ -276,11 +264,6 @@ func Router() *gin.Engine { obligations.DELETE("/classifications/:classification", middleware.RoleBasedAccessMiddleware([]string{"ADMIN"}), DeleteObligationClassification) obligations.POST("/similarity", getSimilarObligations) } - obMap := authorizedv1.Group("/obligation_maps") - { - obMap.PATCH("obligation/:id/license", PatchObligationMap) - obMap.PUT("obligation/:id/license", UpdateLicenseInObligationMap) - } oidcClient := authorizedv1.Group("/oidcClients") { oidcClient.GET("", GetUserOidcClients) diff --git a/pkg/api/licenses.go b/pkg/api/licenses.go index b5e0a78..f71822c 100644 --- a/pkg/api/licenses.go +++ b/pkg/api/licenses.go @@ -185,7 +185,7 @@ func GetLicense(c *gin.Context) { return } - err = db.DB.Where(models.LicenseDB{Id: licenseId}).Preload("User").First(&license).Error + err = db.DB.Where(models.LicenseDB{Id: licenseId}).Preload("User").Preload("Obligations").First(&license).Error if err != nil { er := models.LicenseError{ Status: http.StatusNotFound, @@ -257,30 +257,48 @@ func CreateLicense(c *gin.Context) { lic.UserId = userId _ = db.DB.Transaction(func(tx *gorm.DB) error { - result := tx.Create(&lic) - - if result.Error != nil { + if err := tx.Omit("Obligations").Create(&lic).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to create license", - Error: result.Error.Error(), + Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusInternalServerError, er) - return result.Error + return err } - if err := tx.Preload("User").First(&lic).Error; err != nil { + insertObligations := input.ObligationIds + errs := utils.PerformLicenseMapActions(tx, userId, &lic, insertObligations) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } er := models.LicenseError{ - Status: http.StatusInternalServerError, + Status: http.StatusNotFound, + Message: "Failed to create license", + Error: combinedMapErrors.String(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return errors.New(combinedMapErrors.String()) + } + + if err := tx.Preload("User").Preload("Obligations").First(&lic).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusNotFound, Message: "Failed to create license", - Error: result.Error.Error(), + Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusInternalServerError, er) - return result.Error + return err } if err := utils.AddChangelogsForLicense(tx, userId, &lic, &models.LicenseDB{}); err != nil { @@ -332,10 +350,34 @@ func CreateLicense(c *gin.Context) { // @Security ApiKeyAuth // @Router /licenses/{id} [patch] func UpdateLicense(c *gin.Context) { + var updates models.LicenseUpdateDTO + + if err := c.ShouldBindJSON(&updates); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "invalid json body", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + + if err := validations.Validate.Struct(&updates); err != nil { + er := models.LicenseError{ + Status: http.StatusBadRequest, + Message: "can not update license with these field values", + Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusBadRequest, er) + return + } + _ = db.DB.Transaction(func(tx *gorm.DB) error { - var updates models.LicenseUpdateDTO var oldLicense models.LicenseDB - userId := c.MustGet("userId").(uuid.UUID) licenseId, err := uuid.Parse(c.Param("id")) @@ -350,7 +392,7 @@ func UpdateLicense(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return err } - if err := tx.Where(models.LicenseDB{Id: licenseId}).First(&oldLicense).Error; err != nil { + if err := tx.Preload("User").Preload("Obligations").First(&oldLicense, licenseId).Error; err != nil { er := models.LicenseError{ Status: http.StatusNotFound, Message: fmt.Sprintf("license with id '%s' not found", licenseId.String()), @@ -362,30 +404,6 @@ func UpdateLicense(c *gin.Context) { return err } - if err := c.ShouldBindJSON(&updates); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return err - } - - if err := validations.Validate.Struct(&updates); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "can not update license with these field values", - Error: fmt.Sprintf("field '%s' failed validation: %s\n", err.(validator.ValidationErrors)[0].Field(), err.(validator.ValidationErrors)[0].Tag()), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return err - } - newLicense := updates.ConvertToLicenseDB() if newLicense.Text != nil && *oldLicense.Text != *newLicense.Text && !*oldLicense.TextUpdatable { er := models.LicenseError{ @@ -400,7 +418,7 @@ func UpdateLicense(c *gin.Context) { } // Overwrite values of existing keys, add new key value pairs and remove keys with null values. - if err := tx.Model(&models.LicenseDB{}).Where(models.LicenseDB{Id: oldLicense.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", updates.ExternalRef)).Error; err != nil { + if err := tx.Model(models.LicenseDB{}).Where(models.LicenseDB{Id: oldLicense.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", updates.ExternalRef)).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to update license", @@ -424,6 +442,27 @@ func UpdateLicense(c *gin.Context) { return err } + if updates.ObligationIds != nil { + errs := utils.PerformLicenseMapActions(tx, userId, &oldLicense, *updates.ObligationIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "Failed to update license", + Error: combinedMapErrors.String(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return errors.New(combinedMapErrors.String()) + } + } + if err := tx.Preload("User").Preload("Obligations").Where(models.LicenseDB{Id: oldLicense.Id}).First(&newLicense).Error; err != nil { er := models.LicenseError{ Status: http.StatusInternalServerError, @@ -623,7 +662,8 @@ func ImportLicenses(c *gin.Context) { for i := range licenses { errMessage, importStatus := utils.InsertOrUpdateLicenseOnImport(&licenses[i], userId) - if importStatus == utils.IMPORT_FAILED { + switch importStatus { + case utils.IMPORT_FAILED: erroredElem := "" if licenses[i].Id != nil { erroredElem = (*licenses[i].Id).String() @@ -637,18 +677,46 @@ func ImportLicenses(c *gin.Context) { Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), }) - } else if importStatus == utils.IMPORT_LICENSE_CREATED { + case utils.IMPORT_LICENSE_CREATED: res.Data = append(res.Data, models.LicenseImportStatus{ Shortname: *licenses[i].Shortname, Status: http.StatusCreated, Id: *licenses[i].Id, }) - } else if importStatus == utils.IMPORT_LICENSE_UPDATED { + case utils.IMPORT_LICENSE_UPDATED: res.Data = append(res.Data, models.LicenseImportStatus{ Shortname: *licenses[i].Shortname, Status: http.StatusOK, Id: *licenses[i].Id, }) + case utils.IMPORT_LICENSE_CREATE_OBLIGATION_ASSOCIATION_FAILED: + erroredElem := "" + if licenses[i].Id != nil { + erroredElem = (*licenses[i].Id).String() + } else if licenses[i].Shortname != nil { + erroredElem = *licenses[i].Shortname + } + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusBadRequest, + Message: errMessage, + Error: erroredElem, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + case utils.IMPORT_LICENSE_UPDATE_OBLIGATION_ASSOCIATION_FAILED: + erroredElem := "" + if licenses[i].Id != nil { + erroredElem = (*licenses[i].Id).String() + } else if licenses[i].Shortname != nil { + erroredElem = *licenses[i].Shortname + } + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusBadRequest, + Message: errMessage, + Error: erroredElem, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) } } diff --git a/pkg/api/obligationmap.go b/pkg/api/obligationmap.go deleted file mode 100644 index 94488e7..0000000 --- a/pkg/api/obligationmap.go +++ /dev/null @@ -1,374 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Siemens AG -// SPDX-FileContributor: Gaurav Mishra -// SPDX-FileContributor: Dearsh Oberoi -// -// SPDX-License-Identifier: GPL-2.0-only - -package api - -import ( - "fmt" - "net/http" - "time" - - "golang.org/x/exp/slices" - "gorm.io/gorm" - - "github.com/fossology/LicenseDb/pkg/db" - "github.com/fossology/LicenseDb/pkg/models" - "github.com/fossology/LicenseDb/pkg/utils" - "github.com/gin-gonic/gin" - "github.com/google/uuid" -) - -// GetObligationMapByObligationId retrieves obligation maps for a given obligation id -// -// @Summary Get maps for an obligation -// @Description Get obligation maps for a given obligation id -// @Id GetObligationMapByObligationId -// @Tags Obligations -// @Accept json -// @Produce json -// @Param id path string true "Id of the obligation" -// @Success 200 {object} models.ObligationMapResponse -// @Failure 404 {object} models.LicenseError "No obligation with given id found" -// @Security ApiKeyAuth || {} -// @Router /obligation_maps/obligation/{id} [get] -func GetObligationMapByObligationId(c *gin.Context) { - var obligation models.Obligation - var resObMap models.ObligationMapUser - var list []models.ObligationMapLicenseFormat - - obligationId, err := uuid.Parse(c.Param("id")) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("no obligation with id '%s' exists", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - if err := db.DB.Joins("Classification").Joins("Type").Preload("Licenses").Where(models.Obligation{Id: obligationId}).First(&obligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with id '%s' not found", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - for _, lic := range obligation.Licenses { - list = append(list, models.ObligationMapLicenseFormat{ - Id: lic.Id, - Shortname: *lic.Shortname, - }) - } - - resObMap = models.ObligationMapUser{ - Id: obligation.Id, - Topic: *obligation.Topic, - Type: (*obligation.Type).Type, - Licenses: list, - } - - res := models.ObligationMapResponse{ - Data: []models.ObligationMapUser{resObMap}, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: 1, - }, - } - c.JSON(http.StatusOK, res) -} - -// GetObligationMapByLicenseId retrieves obligation maps for given license id -// -// @Summary Get maps for a license -// @Description Get obligation maps for a given license id -// @Id GetObligationMapByLicenseId -// @Tags Obligations -// @Accept json -// @Produce json -// @Param license path string true "id of the license" -// @Success 200 {object} models.ObligationMapResponse -// @Failure 404 {object} models.LicenseError "No license with given id found" -// @Security ApiKeyAuth || {} -// @Router /obligation_maps/license/{id} [get] -func GetObligationMapByLicenseId(c *gin.Context) { - var license models.LicenseDB - var resObMapList []models.ObligationMapUser - - licenseId, err := uuid.Parse(c.Param("id")) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("no license with id '%s' exists", licenseId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - if err := db.DB.Preload("Obligations").Preload("Obligations.Type").Preload("Obligations.Classification").Where(models.LicenseDB{Id: licenseId}).First(&license).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("no license with id '%s' exists", licenseId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - for _, ob := range license.Obligations { - resObMapList = append(resObMapList, models.ObligationMapUser{ - Id: ob.Id, - Type: (*ob.Type).Type, - Topic: *ob.Topic, - Licenses: []models.ObligationMapLicenseFormat{ - { - Id: license.Id, - Shortname: *license.Shortname, - }, - }, - }) - } - - res := models.ObligationMapResponse{ - Data: resObMapList, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: len(resObMapList), - }, - } - c.JSON(http.StatusOK, res) -} - -// PatchObligationMap Add or remove licenses from obligation map for a given obligation id -// -// @Summary Add or remove licenses from obligation map -// @Description Add or remove licenses from obligation map for a given obligation id -// @Id PatchObligationMap -// @Tags Obligations -// @Accept json -// @Produce json -// @Param id path string true "Id of the obligation" -// @Param license_maps body models.LicenseMapInput true "License ids with action" -// @Success 200 {object} models.ObligationMapResponse -// @Failure 400 {object} models.LicenseError "Invalid json body" -// @Failure 404 {object} models.LicenseError "No license or obligation found." -// @Failure 500 {object} models.LicenseError "Failure to insert new maps" -// @Security ApiKeyAuth -// @Router /obligation_maps/obligation/{id}/license [patch] -func PatchObligationMap(c *gin.Context) { - var obligation models.Obligation - var obMapInput models.LicenseMapInput - var removeLicenses, insertLicenses []uuid.UUID - - obligationId, err := uuid.Parse(c.Param("id")) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("no obligation with id '%s' exists", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - if err := c.ShouldBindJSON(&obMapInput); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - if err := db.DB.Preload("Licenses").Joins("Type").Where(models.Obligation{Id: obligationId}).First(&obligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with id '%s' not found", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - for _, lic := range obMapInput.MapInput { - if lic.Add { - found := false - for _, l := range obligation.Licenses { - if lic.Id == l.Id { - found = true - break - } - } - if !found { - insertLicenses = append(insertLicenses, lic.Id) - } - } else { - removeLicenses = append(removeLicenses, lic.Id) - } - } - - userId := c.MustGet("userId").(uuid.UUID) - _ = db.DB.Transaction(func(tx *gorm.DB) error { - newLicenseAssociations, errs := utils.PerformObligationMapActions(tx, userId, &obligation, removeLicenses, insertLicenses) - if len(errs) != 0 { - var combinedErrors string - for _, err := range errs { - if err != nil { - combinedErrors += fmt.Sprintf("%s\n", err) - } - } - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Unable to add/remove some of the licenses", - Error: combinedErrors, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusInternalServerError, er) - return nil - } - - obMap := &models.ObligationMapUser{ - Topic: *obligation.Topic, - Type: (*obligation.Type).Type, - Licenses: newLicenseAssociations, - } - - res := models.ObligationMapResponse{ - Data: []models.ObligationMapUser{*obMap}, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: 1, - }, - } - - c.JSON(http.StatusOK, res) - - return nil - }) -} - -// UpdateLicenseInObligationMap Update license list of an obligation map -// -// @Summary Change license list -// @Description Replaces the license list of an obligation id with the given list in the obligation map. -// @Id UpdateLicenseInObligationMap -// @Tags Obligations -// @Accept json -// @Produce json -// @Param id path string true "Id of the obligation" -// @Param Ids body models.LicenseListInput true "Ids of the licenses to be in map" -// @Success 200 {object} models.ObligationMapResponse -// @Failure 400 {object} models.LicenseError "Invalid json body" -// @Failure 404 {object} models.LicenseError "No license or obligation found." -// @Security ApiKeyAuth -// @Router /obligation_maps/obligation/{id}/license [put] -func UpdateLicenseInObligationMap(c *gin.Context) { - var obligation models.Obligation - var obMapInput models.LicenseListInput - var removeLicenses, insertLicenses []uuid.UUID - - if err := c.ShouldBindJSON(&obMapInput); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - obligationId, err := uuid.Parse(c.Param("id")) - if err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("no obligation with id '%s' exists", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return - } - - if err := db.DB.Preload("Licenses").Joins("Type").Where(models.Obligation{Id: obligationId}).First(&obligation).Error; err != nil { - er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with id '%s' not found", obligationId.String()), - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusNotFound, er) - return - } - - obMapInput.LicenseIds = slices.Compact(obMapInput.LicenseIds) - - utils.GenerateDiffForReplacingLicenses(&obligation, obMapInput.LicenseIds, &removeLicenses, &insertLicenses) - - userId := c.MustGet("userId").(uuid.UUID) - _ = db.DB.Transaction(func(tx *gorm.DB) error { - newLicenseAssociations, errs := utils.PerformObligationMapActions(tx, userId, &obligation, removeLicenses, insertLicenses) - if len(errs) != 0 { - var combinedErrors string - for _, err := range errs { - if err != nil { - combinedErrors += fmt.Sprintf("%s\n", err) - } - } - er := models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Unable to add/remove some of the licenses", - Error: combinedErrors, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusInternalServerError, er) - return nil - } - - obMap := &models.ObligationMapUser{ - Id: obligation.Id, - Topic: *obligation.Topic, - Type: (*obligation.Type).Type, - Licenses: newLicenseAssociations, - } - - res := models.ObligationMapResponse{ - Data: []models.ObligationMapUser{*obMap}, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: 1, - }, - } - - c.JSON(http.StatusOK, res) - - return nil - }) -} diff --git a/pkg/api/obligations.go b/pkg/api/obligations.go index f969d90..7dbb712 100644 --- a/pkg/api/obligations.go +++ b/pkg/api/obligations.go @@ -10,23 +10,22 @@ package api import ( "encoding/json" + "errors" "fmt" "net/http" "path/filepath" "reflect" + "slices" "strconv" "strings" "time" - "golang.org/x/exp/slices" - "github.com/fossology/LicenseDb/pkg/db" "github.com/fossology/LicenseDb/pkg/models" "github.com/fossology/LicenseDb/pkg/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" - "gorm.io/gorm/clause" ) // GetAllObligation retrieves a list of all obligation records @@ -195,25 +194,11 @@ func CreateObligation(c *gin.Context) { _ = db.DB.Transaction(func(tx *gorm.DB) error { ob := obligation.ConvertToObligation() - result := tx.Omit("Licenses").Create(&ob) - - if result.Error != nil { + if err := tx.Omit("Licenses").Create(&ob).Error; err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, Message: "Failed to create obligation", - Error: result.Error.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - return result.Error - } - - if err := addChangelogsForObligation(tx, userId, &ob, &models.Obligation{}); err != nil { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Failed to create obligation", - Error: result.Error.Error(), + Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } @@ -221,40 +206,61 @@ func CreateObligation(c *gin.Context) { return err } - var combinedMapErrors string - var removeLicenses, insertLicenses []uuid.UUID - insertLicenses = obligation.LicenseIds - _, errs := utils.PerformObligationMapActions(tx, userId, &ob, removeLicenses, insertLicenses) + insertLicenses := obligation.LicenseIds + errs := utils.PerformObligationMapActions(tx, userId, &ob, insertLicenses) if len(errs) != 0 { + var combinedMapErrors strings.Builder for _, err := range errs { if err != nil { - combinedMapErrors += fmt.Sprintf("%s\n", err) + fmt.Fprintf(&combinedMapErrors, "%s\n", err) } } + er := models.LicenseError{ + Status: http.StatusNotFound, + Message: "Failed to create obligation", + Error: combinedMapErrors.String(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + } + c.JSON(http.StatusNotFound, er) + return errors.New(combinedMapErrors.String()) } - if combinedMapErrors == "" { - obdto := ob.ConvertToObligationResponseDTO() - res := models.ObligationResponse{ - Data: []models.ObligationResponseDTO{obdto}, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: 1, - }, + if err := tx.Joins("Classification").Joins("Type").Preload("Licenses").First(&ob, ob.Id).Error; err != nil { + er := models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to create obligation", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), } + c.JSON(http.StatusInternalServerError, er) + return err + } - c.JSON(http.StatusCreated, res) - } else { + if err := addChangelogsForObligation(tx, userId, &ob, &models.Obligation{}); err != nil { er := models.LicenseError{ Status: http.StatusBadRequest, - Message: "Obligation created successfully but license association failed", - Error: combinedMapErrors, + Message: "Failed to create obligation", + Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } c.JSON(http.StatusBadRequest, er) + return err } + obdto := ob.ConvertToObligationResponseDTO() + res := models.ObligationResponse{ + Data: []models.ObligationResponseDTO{obdto}, + Status: http.StatusOK, + Meta: models.PaginationMeta{ + ResourceCount: 1, + }, + } + + c.JSON(http.StatusCreated, res) + return nil }) } @@ -278,7 +284,6 @@ func CreateObligation(c *gin.Context) { func UpdateObligation(c *gin.Context) { var updates models.ObligationUpdateDTO var oldObligation models.Obligation - userId := c.MustGet("userId").(uuid.UUID) obligationId, err := uuid.Parse(c.Param("id")) if err != nil { @@ -293,27 +298,27 @@ func UpdateObligation(c *gin.Context) { return } - if err := db.DB.Joins("Classification").Joins("Type").Preload("Licenses").Where(models.Obligation{Id: obligationId}).First(&oldObligation).Error; err != nil { + if err := c.ShouldBindJSON(&updates); err != nil { er := models.LicenseError{ - Status: http.StatusNotFound, - Message: fmt.Sprintf("obligation with id '%s' not found", obligationId.String()), + Status: http.StatusBadRequest, + Message: "invalid json body", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusNotFound, er) + c.JSON(http.StatusBadRequest, er) return } - if err := c.ShouldBindJSON(&updates); err != nil { + if err := db.DB.Joins("Classification").Joins("Type").Preload("Licenses").Where(models.Obligation{Id: obligationId}).First(&oldObligation).Error; err != nil { er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "invalid json body", + Status: http.StatusNotFound, + Message: fmt.Sprintf("obligation with id '%s' not found", obligationId.String()), Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), } - c.JSON(http.StatusBadRequest, er) + c.JSON(http.StatusNotFound, er) return } @@ -330,7 +335,6 @@ func UpdateObligation(c *gin.Context) { return } newObligation.Id = oldObligation.Id - var combinedMapErrors string if err := db.DB.Transaction(func(tx *gorm.DB) error { // Overwrite values of existing keys, add new key value pairs and remove keys with null values. @@ -342,36 +346,26 @@ func UpdateObligation(c *gin.Context) { return err } - if err := tx.Joins("Type").Joins("Classification").First(&newObligation).Error; err != nil { - return err + userId := c.MustGet("userId").(uuid.UUID) + if updates.LicenseIds != nil { + errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, *updates.LicenseIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + return errors.New(combinedMapErrors.String()) + } } - if err := addChangelogsForObligation(tx, userId, &newObligation, &oldObligation); err != nil { + if err := tx.Joins("Type").Joins("Classification").Preload("Licenses").First(&newObligation).Error; err != nil { return err } - if updates.LicenseIds == nil { - return nil - } - - if err := tx.Where(&models.Obligation{Id: oldObligation.Id}).Preload("Licenses").First(&oldObligation).Error; err != nil { - return nil - } - var licenseIds, removeLicenses, insertLicenses []uuid.UUID - - licenseIds = slices.Compact(*updates.LicenseIds) - - utils.GenerateDiffForReplacingLicenses(&oldObligation, licenseIds, &removeLicenses, &insertLicenses) - - userId := c.MustGet("userId").(uuid.UUID) - _, errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, removeLicenses, insertLicenses) - if len(errs) != 0 { - for _, err := range errs { - if err != nil { - combinedMapErrors += fmt.Sprintf("%s\n", err) - } - } - return nil + if err := addChangelogsForObligation(tx, userId, &newObligation, &oldObligation); err != nil { + return err } return nil @@ -386,28 +380,16 @@ func UpdateObligation(c *gin.Context) { c.JSON(http.StatusBadRequest, er) return } + obDto := newObligation.ConvertToObligationResponseDTO() - if combinedMapErrors != "" { - er := models.LicenseError{ - Status: http.StatusBadRequest, - Message: "Obligation created successfully but license association failed", - Error: combinedMapErrors, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - } - c.JSON(http.StatusBadRequest, er) - } else { - obDto := newObligation.ConvertToObligationResponseDTO() - - res := models.ObligationResponse{ - Data: []models.ObligationResponseDTO{obDto}, - Status: http.StatusOK, - Meta: models.PaginationMeta{ - ResourceCount: 1, - }, - } - c.JSON(http.StatusOK, res) + res := models.ObligationResponse{ + Data: []models.ObligationResponseDTO{obDto}, + Status: http.StatusOK, + Meta: models.PaginationMeta{ + ResourceCount: 1, + }, } + c.JSON(http.StatusOK, res) } // DeleteObligation marks an existing obligation record as inactive @@ -602,16 +584,17 @@ func ImportObligations(c *gin.Context) { oldObligation := ob.ConvertToObligation() newObligation := oldObligation _ = db.DB.Transaction(func(tx *gorm.DB) error { - // If id not present in json object, create a new one. + // (a) If id not present in json object, create a new one. // - // If id present in json object, but entry not found in database (can arise in cases when + // (b) If id present in json object, but entry not found in database (can arise in cases when // license/obligation file exported from one LicenseDb instance is imported in another instance) // then create a new one // - // If id present in json object and entry found in database, update the database with field values + // (c) If id present in json object and entry found in database, update the database with field values // of the json object if ob.Id == nil { - result := tx.Create(&oldObligation) + // Case (a) + result := tx.Omit("Licenses").Create(&oldObligation) if result.Error != nil { res.Data = append(res.Data, models.LicenseError{ Status: http.StatusBadRequest, @@ -623,45 +606,34 @@ func ImportObligations(c *gin.Context) { return err } - if err := addChangelogsForObligation(tx, userId, &oldObligation, &models.Obligation{}); err != nil { - res.Data = append(res.Data, models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Failed to update license", - Error: err.Error(), - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - }) - return err + var message string + if ob.LicenseIds != nil { + userId := c.MustGet("userId").(uuid.UUID) + errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, *ob.LicenseIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + message = fmt.Sprintf("Obligation created successfully but there was en error creating license associations: %s", combinedMapErrors.String()) + } } - // case when obligation doesn't exist in database and is inserted - res.Data = append(res.Data, models.ObligationImportStatus{ - Data: models.ObligationId{Id: oldObligation.Id, Topic: *oldObligation.Topic}, - Status: http.StatusCreated, - Message: "obligation created successfully", - }) - } else { - if err := tx.Where(&models.Obligation{Id: oldObligation.Id}).First(&oldObligation).Error; err != nil { - res.Data = append(res.Data, models.LicenseError{ + + if err := tx.Joins("Classification").Joins("Type").Preload("Licenses").First(&oldObligation, oldObligation.Id).Error; err != nil { + er := models.LicenseError{ Status: http.StatusInternalServerError, - Message: "Error updating license associations", + Message: "Failed to create obligation", Error: err.Error(), Path: c.Request.URL.Path, Timestamp: time.Now().Format(time.RFC3339), - }) - return nil - } - newObligation.Id = oldObligation.Id - if err := tx.Omit("Licenses", "Topic").Clauses(clause.Returning{}).Updates(&newObligation).Error; err != nil { - res.Data = append(res.Data, models.LicenseError{ - Status: http.StatusBadRequest, - Message: fmt.Sprintf("Failed to update obligation: %s", err.Error()), - Error: *ob.Topic, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - }) + } + c.JSON(http.StatusInternalServerError, er) return err } - if err := addChangelogsForObligation(tx, userId, &newObligation, &oldObligation); err != nil { + + if err := addChangelogsForObligation(tx, userId, &oldObligation, &models.Obligation{}); err != nil { res.Data = append(res.Data, models.LicenseError{ Status: http.StatusInternalServerError, Message: "Failed to update license", @@ -671,50 +643,162 @@ func ImportObligations(c *gin.Context) { }) return err } - + if message == "" { + message = "obligation created successfully" + } res.Data = append(res.Data, models.ObligationImportStatus{ - Data: models.ObligationId{Id: *ob.Id, Topic: *ob.Topic}, - Status: http.StatusOK, - Message: "obligation updated successfully", + Data: models.ObligationId{Id: oldObligation.Id, Topic: *oldObligation.Topic}, + Status: http.StatusCreated, + Message: message, }) - } + } else { + if err := tx.Where(&models.Obligation{Id: oldObligation.Id}).First(&oldObligation).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + result := tx.Omit("Licenses").Create(&oldObligation) + if result.Error != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusBadRequest, + Message: fmt.Sprintf("Failed to create/update obligation: %s", result.Error.Error()), + Error: *ob.Topic, + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + + var message string + if ob.LicenseIds != nil { + userId := c.MustGet("userId").(uuid.UUID) + errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, *ob.LicenseIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + message = fmt.Sprintf("Obligation created successfully but there was en error creating license associations: %s", combinedMapErrors.String()) + } + } + + if err := tx.Joins("Classification").Joins("Type").Preload("Licenses").First(&oldObligation, oldObligation.Id).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + + if err := addChangelogsForObligation(tx, userId, &oldObligation, &models.Obligation{}); err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + if message == "" { + message = "obligation created successfully" + } + res.Data = append(res.Data, models.ObligationImportStatus{ + Data: models.ObligationId{Id: oldObligation.Id, Topic: *oldObligation.Topic}, + Status: http.StatusCreated, + Message: message, + }) + } else { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Error updating license associations", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return nil + } + } else { + if newObligation.Text != nil && *oldObligation.Text != *newObligation.Text && !*oldObligation.TextUpdatable { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusBadRequest, + Message: "Text is not updatable", + Error: "Field `text_updatable` needs to be true to update the text", + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return errors.New("field `text_updatable` needs to be true to update the text") + } + if err := tx.Model(&models.Obligation{}).Where(models.Obligation{Id: oldObligation.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", ob.ExternalRef)).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } - if ob.LicenseIds == nil { - return nil - } - // do not return error so that obligations - // are created/updated even if the association fails(license is not found etc) - if err := tx.Where(&models.Obligation{Id: oldObligation.Id}).Preload("Licenses").First(&oldObligation).Error; err != nil { - return nil - } - var licenseIds, removeLicenses, insertLicenses []uuid.UUID + if err := tx.Omit("ExternalRef", "Licenses", "Topic").Updates(&newObligation).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } + + var message string + if ob.LicenseIds != nil { + userId := c.MustGet("userId").(uuid.UUID) + errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, *ob.LicenseIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + message = fmt.Sprintf("Obligation updated successfully but there was en error updating license associations: %s", combinedMapErrors.String()) + } + } - licenseIds = slices.Compact(*ob.LicenseIds) + if err := tx.Joins("Type").Joins("Classification").Preload("Licenses").First(&newObligation).Error; err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } - utils.GenerateDiffForReplacingLicenses(&oldObligation, licenseIds, &removeLicenses, &insertLicenses) + if err := addChangelogsForObligation(tx, userId, &newObligation, &oldObligation); err != nil { + res.Data = append(res.Data, models.LicenseError{ + Status: http.StatusInternalServerError, + Message: "Failed to update license", + Error: err.Error(), + Path: c.Request.URL.Path, + Timestamp: time.Now().Format(time.RFC3339), + }) + return err + } - userId := c.MustGet("userId").(uuid.UUID) - _, errs := utils.PerformObligationMapActions(tx, userId, &oldObligation, removeLicenses, insertLicenses) - if len(errs) != 0 { - var combinedErrors string - for _, err := range errs { - if err != nil { - combinedErrors += fmt.Sprintf("%s\n", err) + if message == "" { + message = "obligation updated successfully" } + res.Data = append(res.Data, models.ObligationImportStatus{ + Data: models.ObligationId{Id: *ob.Id, Topic: *ob.Topic}, + Status: http.StatusOK, + Message: message, + }) } - res.Data = append(res.Data, models.LicenseError{ - Status: http.StatusInternalServerError, - Message: "Error updating license associations", - Error: combinedErrors, - Path: c.Request.URL.Path, - Timestamp: time.Now().Format(time.RFC3339), - }) - } else { - res.Data = append(res.Data, models.ObligationImportStatus{ - Data: models.ObligationId{Id: oldObligation.Id, Topic: *ob.Topic}, - Status: http.StatusOK, - Message: "licenses associated with obligations successfully", - }) } return nil @@ -771,6 +855,17 @@ func ExportObligations(c *gin.Context) { // addChangelogsForObligation adds changelogs for the updated fields on obligation update func addChangelogsForObligation(tx *gorm.DB, userId uuid.UUID, newObligation, oldObligation *models.Obligation) error { + uuidsToStr := func(ids []models.LicenseDB) string { + if len(ids) == 0 { + return "" + } + s := make([]string, 0, len(ids)) + for _, lic := range ids { + s = append(s, lic.Id.String()) + } + slices.Sort(s) + return strings.Join(s, ", ") + } var changes []models.ChangeLog var oldType, newType *string @@ -828,6 +923,11 @@ func addChangelogsForObligation(tx *gorm.DB, userId uuid.UUID, } } + oldVal := uuidsToStr(oldObligation.Licenses) + newVal := uuidsToStr(newObligation.Licenses) + + utils.AddChangelog("Licenses", &oldVal, &newVal, &changes) + if len(changes) != 0 { audit := models.Audit{ UserId: userId, diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 389031b..886f665 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -499,7 +499,7 @@ func UpdateUser(c *gin.Context) { res := models.UserResponse{ Data: []models.User{updatedUser}, - Status: http.StatusCreated, + Status: http.StatusOK, Meta: &models.PaginationMeta{ ResourceCount: 1, }, diff --git a/pkg/models/licenses.go b/pkg/models/licenses.go index 075458f..c7c9653 100644 --- a/pkg/models/licenses.go +++ b/pkg/models/licenses.go @@ -105,11 +105,11 @@ func (l *LicenseDB) ConvertToLicenseResponseDTO() LicenseResponseDTO { response.Url = *l.Url response.User = l.User - obligations := []string{} + obligations := []uuid.UUID{} for _, o := range l.Obligations { - obligations = append(obligations, *o.Topic) + obligations = append(obligations, o.Id) } - response.Obligations = obligations + response.ObligationIds = obligations return response } @@ -129,7 +129,7 @@ type LicenseCreateDTO struct { SpdxId string `json:"spdx_id" validate:"required,spdxId" example:"MIT"` Risk *int64 `json:"risk" validate:"min=0,max=5" example:"1"` ExternalRef LicenseDBSchemaExtension `json:"external_ref"` - Obligations *[]uuid.UUID `json:"obligations" swaggertype:"array,string" example:"f81d4fae-7dec-11d0-a765-00a0c91e6bf6,f812jfae-7dbc-11d0-a765-00a0hf06bf6"` + ObligationIds []uuid.UUID `json:"obligation_ids" swaggertype:"array,string" example:"f81d4fae-7dec-11d0-a765-00a0c91e6bf6,f812jfae-7dbc-11d0-a765-00a0hf06bf6"` } func (dto *LicenseCreateDTO) ConvertToLicenseDB() LicenseDB { @@ -149,14 +149,6 @@ func (dto *LicenseCreateDTO) ConvertToLicenseDB() LicenseDB { l.TextUpdatable = dto.TextUpdatable l.Url = dto.Url - if dto.Obligations != nil { - obligations := []Obligation{} - for _, id := range *dto.Obligations { - obligations = append(obligations, Obligation{Id: id}) - } - l.Obligations = obligations - } - return l } @@ -176,7 +168,7 @@ type LicenseResponseDTO struct { SpdxId string `json:"spdx_id" example:"MIT"` Risk int64 `json:"risk" example:"1"` ExternalRef LicenseDBSchemaExtension `json:"external_ref"` - Obligations []string `json:"obligations"` + ObligationIds []uuid.UUID `json:"obligation_ids"` User User `json:"created_by"` AddDate time.Time `json:"add_date"` } @@ -196,7 +188,7 @@ type LicenseUpdateDTO struct { SpdxId *string `json:"spdx_id" example:"MIT" validate:"omitempty,spdxId"` Risk *int64 `json:"risk" validate:"omitempty,min=0,max=5" example:"1"` ExternalRef map[string]interface{} `json:"external_ref"` - Obligations *[]Obligation `json:"obligations"` + ObligationIds *[]uuid.UUID `json:"obligation_ids"` } func (dto *LicenseUpdateDTO) ConvertToLicenseDB() LicenseDB { @@ -234,11 +226,14 @@ type LicenseImportDTO struct { SpdxId *string `json:"spdx_id" example:"MIT" validate:"omitempty,spdxId"` Risk *int64 `json:"risk" validate:"omitempty,min=0,max=5" example:"1"` ExternalRef map[string]interface{} `json:"external_ref"` - Obligations *[]Obligation `json:"obligations"` + ObligationIds *[]uuid.UUID `json:"obligation_ids"` } func (dto *LicenseImportDTO) ConvertToLicenseDB() LicenseDB { var l LicenseDB + if dto.Id != nil { + l.Id = *dto.Id + } l.Shortname = dto.Shortname l.Active = dto.Active l.Copyleft = dto.Copyleft @@ -368,3 +363,9 @@ type LicenseResponse struct { Data []LicenseResponseDTO `json:"data"` Meta *PaginationMeta `json:"paginationmeta"` } + +type LicenseMapObligationFormat struct { + Id uuid.UUID `json:"id"` + Topic string `json:"topic"` + Type string `json:"type"` +} diff --git a/pkg/models/obligations.go b/pkg/models/obligations.go index 0ad9ab6..5cb0848 100644 --- a/pkg/models/obligations.go +++ b/pkg/models/obligations.go @@ -305,7 +305,9 @@ type ObligationFileDTO struct { func (obDto *ObligationFileDTO) ConvertToObligation() Obligation { var o Obligation - + if obDto.Id != nil { + o.Id = *obDto.Id + } o.Topic = obDto.Topic if obDto.Type != nil { o.Type = &ObligationType{Type: *obDto.Type} diff --git a/pkg/models/types.go b/pkg/models/types.go index 91f5b02..7efebe7 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -261,37 +261,6 @@ type ObligationMapLicenseFormat struct { Shortname string `json:"shortname" example:"MIT"` } -// ObligationMapUser Structure with obligation topic and license shortname list, a simple representation for user. -type ObligationMapUser struct { - Id uuid.UUID `json:"id" example:"f81d4fae-7dec-11d0-a765-00a0c91e6bf6" swaggertype:"string"` - Topic string `json:"topic" example:"copyleft"` - Type string `json:"type" example:"obligation" enums:"obligation,restriction,risk,right"` - Licenses []ObligationMapLicenseFormat `json:"licenses"` -} - -// LicenseShortnamesInput represents the input format for adding/removing licenses from obligation map. -type LicenseListInput struct { - LicenseIds []uuid.UUID `json:"ids" swaggertype:"array,object"` -} - -// LicenseMapElement Element to hold license -type LicenseMapElement struct { - Id uuid.UUID `json:"id" example:"f81d4fae-7dec-11d0-a765-00a0c91e6bf6" swaggertype:"string"` - Add bool `json:"add" example:"true"` -} - -// LicenseMapInput List of elements to be read as input by API -type LicenseMapInput struct { - MapInput []LicenseMapElement `json:"map"` -} - -// ObligationMapResponse response format for obligation map data. -type ObligationMapResponse struct { - Status int `json:"status" example:"200"` - Data []ObligationMapUser `json:"data"` - Meta PaginationMeta `json:"paginationmeta"` -} - // ObligationImportRequest represents the request body structure for import obligation type ObligationImportRequest struct { ObligationFile string `form:"file"` diff --git a/pkg/utils/util.go b/pkg/utils/util.go index f01be78..e914f1c 100644 --- a/pkg/utils/util.go +++ b/pkg/utils/util.go @@ -16,6 +16,7 @@ import ( "net/http" "os" "reflect" + "slices" "strconv" "strings" "time" @@ -114,6 +115,8 @@ const ( IMPORT_FAILED LicenseImportStatusCode = iota + 1 IMPORT_LICENSE_CREATED IMPORT_LICENSE_UPDATED + IMPORT_LICENSE_CREATE_OBLIGATION_ASSOCIATION_FAILED + IMPORT_LICENSE_UPDATE_OBLIGATION_ASSOCIATION_FAILED ) func InsertOrUpdateLicenseOnImport(lic *models.LicenseImportDTO, userId uuid.UUID) (string, LicenseImportStatusCode) { @@ -128,56 +131,153 @@ func InsertOrUpdateLicenseOnImport(lic *models.LicenseImportDTO, userId uuid.UUI _ = db.DB.Transaction(func(tx *gorm.DB) error { license := lic.ConvertToLicenseDB() - license.UserId = userId - + /* + We can have the following situations here: + 1. The license import object has an id, and, + (a) There is a license corresponding to that id in the database: Update the license with the new entries + (b) There is no license corresponding to that id in the database: License is being imported to a new server, + add it in database with the same id + 2. The license import object does not have an id: A new license is being created, we add it to the database. + */ if lic.Id != nil { var newLicense, oldLicense models.LicenseDB - if err := tx.Where(models.LicenseDB{Id: *lic.Id}).First(&oldLicense).Error; err != nil { - message = fmt.Sprintf("cannot find license: %s", err.Error()) - importStatus = IMPORT_FAILED - return errors.New(message) - } - newLicense = license + if err := tx.Where(models.LicenseDB{Id: *lic.Id}).Preload("User").Preload("Obligations").First(&oldLicense).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // case 1(b) + license.UserId = userId + if err := tx.Omit("Obligations").Create(&license).Error; err != nil { + message = fmt.Sprintf("failed to import license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + + if lic.ObligationIds != nil { + errs := PerformLicenseMapActions(tx, userId, &license, *lic.ObligationIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + importStatus = IMPORT_LICENSE_CREATE_OBLIGATION_ASSOCIATION_FAILED + message = fmt.Sprintf("successfully created license with id %s, failed to associate obligations: %s", license.Id, combinedMapErrors.String()) + } + } + + if err := tx.Preload("User").Preload("Obligations").First(&license).Error; err != nil { + message = fmt.Sprintf("failed to create license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + + if err := AddChangelogsForLicense(tx, userId, &license, &models.LicenseDB{}); err != nil { + message = fmt.Sprintf("failed to create license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + + if importStatus != 0 { + // for setting api response + lic.Id = &license.Id + lic.Shortname = license.Shortname + importStatus = IMPORT_LICENSE_CREATED + } + } else { + message = fmt.Sprintf("cannot find license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + } else { + // Case 1(a) + newLicense = license + + if *oldLicense.Text != *newLicense.Text { + if !*oldLicense.TextUpdatable { + message = "Field `text_updatable` needs to be true to update the text" + importStatus = IMPORT_FAILED + return errors.New("Field `text_updatable` needs to be true to update the text") + } + } - if *oldLicense.Text != *newLicense.Text { - if !*oldLicense.TextUpdatable { - message = "Field `text_updatable` needs to be true to update the text" + // Overwrite values of existing keys, add new key value pairs and remove keys with null values. + if err := tx.Model(models.LicenseDB{}).Where(models.LicenseDB{Id: oldLicense.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", lic.ExternalRef)).Error; err != nil { + message = fmt.Sprintf("failed to update license: %s", err.Error()) importStatus = IMPORT_FAILED - return errors.New("Field `text_updatable` needs to be true to update the text") + return errors.New(message) } - } - // Overwrite values of existing keys, add new key value pairs and remove keys with null values. - if err := tx.Model(&models.LicenseDB{}).Where(models.LicenseDB{Id: oldLicense.Id}).UpdateColumn("external_ref", gorm.Expr("jsonb_strip_nulls(COALESCE(external_ref, '{}'::jsonb) || ?)", &lic.ExternalRef)).Error; err != nil { - message = fmt.Sprintf("failed to update license: %s", err.Error()) - importStatus = IMPORT_FAILED - return errors.New(message) - } + if err := tx.Omit("ExternalRef", "Obligations", "User").Where(models.LicenseDB{Id: oldLicense.Id}).Updates(&newLicense).Error; err != nil { + message = fmt.Sprintf("failed to update license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } - // Update all other fields except external_ref and rf_shortname - query := tx.Where(&models.LicenseDB{Id: oldLicense.Id}).Omit("ExternalRef", "Obligations", "User") + if lic.ObligationIds != nil { + errs := PerformLicenseMapActions(tx, userId, &oldLicense, *lic.ObligationIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + importStatus = IMPORT_LICENSE_UPDATE_OBLIGATION_ASSOCIATION_FAILED + message = fmt.Sprintf("successfully updated license with id %s, failed to associate obligations: %s", license.Id, combinedMapErrors.String()) + } + } + + if err := tx.Preload("User").Preload("Obligations").Where(models.LicenseDB{Id: oldLicense.Id}).First(&newLicense).Error; err != nil { + message = fmt.Sprintf("failed to update license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + + if err := AddChangelogsForLicense(tx, userId, &newLicense, &oldLicense); err != nil { + message = fmt.Sprintf("failed to update license: %s", err.Error()) + importStatus = IMPORT_FAILED + return errors.New(message) + } + + // for setting api response + lic.Id = &newLicense.Id + lic.Shortname = newLicense.Shortname - if err := query.Clauses(clause.Returning{}).Updates(&newLicense).Scan(&newLicense).Error; err != nil { - message = fmt.Sprintf("failed to update license: %s", err.Error()) + if importStatus != 0 { + importStatus = IMPORT_LICENSE_UPDATED + } + } + } else { + // Case 2 + license.UserId = userId + if err := tx.Omit("Obligations").Create(&license).Error; err != nil { + message = fmt.Sprintf("failed to import license: %s", err.Error()) importStatus = IMPORT_FAILED return errors.New(message) } - // for setting api response - lic.Id = &newLicense.Id - lic.Shortname = newLicense.Shortname + if lic.ObligationIds != nil { + errs := PerformLicenseMapActions(tx, userId, &license, *lic.ObligationIds) + if len(errs) != 0 { + var combinedMapErrors strings.Builder + for _, err := range errs { + if err != nil { + fmt.Fprintf(&combinedMapErrors, "%s\n", err) + } + } + importStatus = IMPORT_LICENSE_CREATE_OBLIGATION_ASSOCIATION_FAILED + message = fmt.Sprintf("successfully created license with id %s, failed to associate obligations: %s", license.Id, combinedMapErrors.String()) + } + } - if err := AddChangelogsForLicense(tx, userId, &newLicense, &oldLicense); err != nil { - message = fmt.Sprintf("failed to update license: %s", err.Error()) + if err := tx.Preload("User").Preload("Obligations").First(&license).Error; err != nil { + message = fmt.Sprintf("failed to create license: %s", err.Error()) importStatus = IMPORT_FAILED return errors.New(message) } - importStatus = IMPORT_LICENSE_UPDATED - } else { - // case when license doesn't exist in database and is inserted - if err := tx.Create(&license).Error; err != nil { - message = fmt.Sprintf("failed to import license: %s", err.Error()) + if err := AddChangelogsForLicense(tx, userId, &license, &models.LicenseDB{}); err != nil { + message = fmt.Sprintf("failed to create license: %s", err.Error()) importStatus = IMPORT_FAILED return errors.New(message) } @@ -186,15 +286,10 @@ func InsertOrUpdateLicenseOnImport(lic *models.LicenseImportDTO, userId uuid.UUI lic.Id = &license.Id lic.Shortname = license.Shortname - if err := AddChangelogsForLicense(tx, userId, &license, &models.LicenseDB{}); err != nil { - message = fmt.Sprintf("failed to create license: %s", err.Error()) - importStatus = IMPORT_FAILED - return errors.New(message) + if importStatus != 0 { + importStatus = IMPORT_LICENSE_CREATED } - - importStatus = IMPORT_LICENSE_CREATED } - return nil }) @@ -216,138 +311,48 @@ func InsertOrUpdateLicenseOnImport(lic *models.LicenseImportDTO, userId uuid.UUI return message, importStatus } -// GenerateDiffForReplacingLicenses creates list of license associations to be created and deleted such that the list of currently associated -// licenses to a obligation is overwritten by the list provided in the param newLicenseAssociations -func GenerateDiffForReplacingLicenses(obligation *models.Obligation, newLicenseAssociations []uuid.UUID, removeLicenses, insertLicenses *[]uuid.UUID) { - // if license in currently associated with the obligation but isn't in newLicenseAssociations, remove it - for _, lic := range obligation.Licenses { - found := false - for _, id := range newLicenseAssociations { - if id == lic.Id { - found = true - break - } - } - if !found { - *removeLicenses = append(*removeLicenses, lic.Id) - } - } - - // if license in newLicenseAssociations but not currently associated with the obligation, insert it - for _, id := range newLicenseAssociations { - found := false - for _, lic := range obligation.Licenses { - if id == lic.Id { - found = true - break - } - } - if !found { - *insertLicenses = append(*insertLicenses, id) - } - } -} - -// PerformObligationMapActions created associations for licenses in insertLicenses and deletes -// associations for licenses in removeLicenses. It also calculates changelog for the changes. -// It returns the final list of associated licenses. -func PerformObligationMapActions(tx *gorm.DB, userId uuid.UUID, obligation *models.Obligation, - removeLicenses, insertLicenses []uuid.UUID) ([]models.ObligationMapLicenseFormat, []error) { - createLicenseAssociations := []models.LicenseDB{} - deleteLicenseAssociations := []models.LicenseDB{} - var oldLicenseAssociations, newLicenseAssociations []models.ObligationMapLicenseFormat +// PerformObligationMapActions replaces current associated licenses with the list of licenses whose ids are provided in the newLicenseIds. +func PerformObligationMapActions(tx *gorm.DB, userId uuid.UUID, obligation *models.Obligation, newLicenseIds []uuid.UUID) []error { + newLicenseAssociations := []models.LicenseDB{} var errs []error - for _, lic := range obligation.Licenses { - oldLicenseAssociations = append(oldLicenseAssociations, models.ObligationMapLicenseFormat{Id: lic.Id, Shortname: *lic.Shortname}) - } - - for _, licId := range insertLicenses { + for _, licId := range newLicenseIds { var license models.LicenseDB - if err := tx.Where(models.LicenseDB{Id: licId}).First(&license).Error; err != nil { + activeStatus := true + if err := tx.Where(models.LicenseDB{Id: licId, Active: &activeStatus}).First(&license).Error; err != nil { errs = append(errs, fmt.Errorf("unable to associate license '%s' to obligation '%s': %s", licId, obligation.Id, err.Error())) } else { - createLicenseAssociations = append(createLicenseAssociations, license) - } - } - - for _, licId := range removeLicenses { - var license models.LicenseDB - if err := tx.Where(models.LicenseDB{Id: licId}).First(&license).Error; err != nil { - errs = append(errs, fmt.Errorf("unable to remove license '%s' from obligation '%s': %s", licId, obligation.Id, err.Error())) - } else { - deleteLicenseAssociations = append(deleteLicenseAssociations, license) + newLicenseAssociations = append(newLicenseAssociations, license) } } - if err := tx.Transaction(func(tx1 *gorm.DB) error { - - if len(createLicenseAssociations) != 0 || len(deleteLicenseAssociations) != 0 { - if err := tx1.Model(obligation).Association("Licenses").Append(createLicenseAssociations); err != nil { - return err - } - if err := tx1.Model(obligation).Association("Licenses").Delete(deleteLicenseAssociations); err != nil { - return err - } - } - - if err := tx. - Preload("Licenses"). - Joins("Type"). - Joins("Classification"). - Where(&models.Obligation{Id: obligation.Id}). - First(&obligation).Error; err != nil { - return err - } - - for _, lic := range obligation.Licenses { - newLicenseAssociations = append(newLicenseAssociations, models.ObligationMapLicenseFormat{Id: lic.Id, Shortname: *lic.Shortname}) - } - - return createObligationMapChangelog(tx, userId, newLicenseAssociations, oldLicenseAssociations, obligation) - }); err != nil { + if err := tx.Model(obligation).Association("Licenses").Replace(newLicenseAssociations); err != nil { errs = append(errs, err) } - return newLicenseAssociations, errs -} - -// createObligationMapChangelog creates the changelog for the obligation map changes. -func createObligationMapChangelog( - tx *gorm.DB, - userId uuid.UUID, - newLicenseAssociations, oldLicenseAssociations []models.ObligationMapLicenseFormat, - obligation *models.Obligation, -) error { - uuidsToStr := func(ids []models.ObligationMapLicenseFormat) string { - if len(ids) == 0 { - return "" - } - s := make([]string, 0, len(ids)) - for _, lic := range ids { - s = append(s, lic.Id.String()) - } - return strings.Join(s, ", ") - } - oldVal := uuidsToStr(oldLicenseAssociations) - newVal := uuidsToStr(newLicenseAssociations) + return errs +} - var changes []models.ChangeLog - AddChangelog("Licenses", &oldVal, &newVal, &changes) +// PerformLicenseMapActions replaces current associated obligations with the list of obligations whose ids are provided in the newObligationIds +func PerformLicenseMapActions(tx *gorm.DB, userId uuid.UUID, license *models.LicenseDB, newObligationIds []uuid.UUID) []error { + newObligationAssociations := []models.Obligation{} + var errs []error - audit := models.Audit{ - UserId: userId, - TypeId: obligation.Id, - Timestamp: time.Now(), - Type: "OBLIGATION", - ChangeLogs: changes, + for _, obId := range newObligationIds { + var ob models.Obligation + activeStatus := true + if err := tx.Where(models.Obligation{Id: obId, Active: &activeStatus}).Preload("Classification").Preload("Type").First(&ob).Error; err != nil { + errs = append(errs, fmt.Errorf("unable to associate obligation '%s': %s", obId, err.Error())) + } else { + newObligationAssociations = append(newObligationAssociations, ob) + } } - if err := tx.Create(&audit).Error; err != nil { - return err + if err := tx.Debug().Model(license).Association("Obligations").Replace(newObligationAssociations); err != nil { + errs = append(errs, err) } - return nil + return errs } func AddChangelogForObligationType(tx *gorm.DB, userId uuid.UUID, oldObType, newObType *models.ObligationType) error { @@ -712,6 +717,17 @@ func AddChangelog[T any](fieldName string, oldValue, newValue *T, changes *[]mod // AddChangelogsForLicense adds changelogs for the updated fields on license update func AddChangelogsForLicense(tx *gorm.DB, userId uuid.UUID, newLicense, oldLicense *models.LicenseDB) error { + uuidsToStr := func(ids []models.Obligation) string { + if len(ids) == 0 { + return "" + } + s := make([]string, 0, len(ids)) + for _, lic := range ids { + s = append(s, lic.Id.String()) + } + slices.Sort(s) + return strings.Join(s, ", ") + } var changes []models.ChangeLog AddChangelog("Fullname", oldLicense.Fullname, newLicense.Fullname, &changes) @@ -726,6 +742,11 @@ func AddChangelogsForLicense(tx *gorm.DB, userId uuid.UUID, AddChangelog("Spdx Id", oldLicense.SpdxId, newLicense.SpdxId, &changes) AddChangelog("Risk", oldLicense.Risk, newLicense.Risk, &changes) + oldVal := uuidsToStr(oldLicense.Obligations) + newVal := uuidsToStr(newLicense.Obligations) + + AddChangelog("Obligation Ids", &oldVal, &newVal, &changes) + oldLicenseExternalRef := oldLicense.ExternalRef.Data() oldExternalRefVal := reflect.ValueOf(oldLicenseExternalRef) typesOf := oldExternalRefVal.Type() diff --git a/tests/licenses_comprehensive_test.go b/tests/licenses_comprehensive_test.go index a8a763b..1f69af2 100644 --- a/tests/licenses_comprehensive_test.go +++ b/tests/licenses_comprehensive_test.go @@ -14,6 +14,7 @@ import ( "github.com/fossology/LicenseDb/pkg/api" "github.com/fossology/LicenseDb/pkg/models" + "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -228,6 +229,73 @@ func TestImportLicenses(t *testing.T) { w := makeRequest("POST", "/licenses/import", nil, true) assert.Equal(t, http.StatusBadRequest, w.Code) }) + + t.Run("importWithObligations", func(t *testing.T) { + // Create a dummy obligation first + dto := models.ObligationCreateDTO{ + Topic: "test-topic-license-import", + Type: "RIGHT", + Text: "some text", + Classification: "GREEN", + Comment: ptr("try to link obligations to a license in POST request"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + ExternalRef: models.ObligationSchemaExtension{ + ObligationExplanation: ptr("this is a test explaination to test the external ref functionality"), + }, + } + wObligation := makeRequest("POST", "/obligations", dto, true) + assert.Equal(t, http.StatusCreated, wObligation.Code, "Failed to create test obligation") + + var obligationRes models.ObligationResponse + err := json.Unmarshal(wObligation.Body.Bytes(), &obligationRes) + assert.NoError(t, err, "Failed to unmarshal obligation response") + assert.NotEmpty(t, obligationRes, "Obligation response data is empty") + createdObligationID := obligationRes.Data[0].Id + + licenses := []models.LicenseImportDTO{ + { + Shortname: ptr("IMPORT-TEST-OBL"), + Fullname: ptr("Import Test License OBL"), + Text: ptr("Test license text for import"), + Url: ptr("https://example.com/import1"), + Notes: ptr("Test notes for import"), + Source: ptr("test"), + SpdxId: ptr("LicenseRef-IMPORT-TEST-OBL"), + Risk: ptr(int64(2)), + ObligationIds: ptr([]uuid.UUID{createdObligationID}), + }, + } + + jsonData, err := json.Marshal(licenses) + assert.NoError(t, err) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "licenses.json") + assert.NoError(t, err) + _, err = part.Write(jsonData) + assert.NoError(t, err) + writer.Close() + + fullPath := baseURL + "/licenses/import" + req := httptest.NewRequest("POST", fullPath, body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+AuthToken) + w := httptest.NewRecorder() + api.Router().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var res models.ImportLicensesResponse + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling JSON: %v", err) + return + } + assert.Equal(t, http.StatusOK, res.Status) + assert.GreaterOrEqual(t, len(res.Data), 0) + }) } func TestSearchInLicense(t *testing.T) { diff --git a/tests/licenses_test.go b/tests/licenses_test.go index e1d0ca3..8023212 100644 --- a/tests/licenses_test.go +++ b/tests/licenses_test.go @@ -68,6 +68,75 @@ func TestCreateLicense(t *testing.T) { w := makeRequest("POST", "/licenses", license, false) assert.Equal(t, http.StatusUnauthorized, w.Code) }) + + t.Run("error_with_wrong_obligations", func(t *testing.T) { + licenseWithObligation := models.LicenseCreateDTO{ + Shortname: "MIT-W", + Fullname: "MIT-WithObligations", + Text: `MIT1 License copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`, + Url: ptr("https://opensource.org/licenses/MIT"), + Notes: ptr("This license is OSI approved."), + Source: ptr("spdx"), + SpdxId: "LicenseRef-MIT-W", + Risk: ptr(int64(2)), + ObligationIds: []uuid.UUID{uuid.New()}, + } + wLicense := makeRequest("POST", "/licenses", licenseWithObligation, true) + assert.Equal(t, http.StatusNotFound, wLicense.Code, "Failed to throw error on license creation with wrong obligations") + }) + + t.Run("success_with_obligations", func(t *testing.T) { + // Create a dummy obligation first + dto := models.ObligationCreateDTO{ + Topic: "test-topic-license", + Type: "RIGHT", + Text: "some text", + Classification: "GREEN", + Comment: ptr("try to link obligations to a license in POST request"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + ExternalRef: models.ObligationSchemaExtension{ + ObligationExplanation: ptr("this is a test explaination to test the external ref functionality"), + }, + } + wObligation := makeRequest("POST", "/obligations", dto, true) + assert.Equal(t, http.StatusCreated, wObligation.Code, "Failed to create test obligation") + + var obligationRes models.ObligationResponse + err := json.Unmarshal(wObligation.Body.Bytes(), &obligationRes) + assert.NoError(t, err, "Failed to unmarshal obligation response") + assert.NotEmpty(t, obligationRes, "Obligation response data is empty") + createdObligationID := obligationRes.Data[0].Id + + licenseWithObligation := models.LicenseCreateDTO{ + Shortname: "MIT-W", + Fullname: "MIT-WithObligations", + Text: `MIT1 License copyright (c) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`, + Url: ptr("https://opensource.org/licenses/MIT"), + Notes: ptr("This license is OSI approved."), + Source: ptr("spdx"), + SpdxId: "LicenseRef-MIT-W", + Risk: ptr(int64(2)), + ObligationIds: []uuid.UUID{createdObligationID}, + } + // Create the license + wLicense := makeRequest("POST", "/licenses", licenseWithObligation, true) + assert.Equal(t, http.StatusCreated, wLicense.Code, "Failed to create license with obligations") + + var res models.LicenseResponse + if err := json.Unmarshal(wLicense.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling license response: %v", err) + return + } + if len(res.Data) == 0 { + t.Fatal("License response data is empty, cannot validate fields") + } + + assert.NotEmpty(t, res.Data[0].ObligationIds, "Expected obligations to be attached to the license") + assert.Len(t, res.Data[0].ObligationIds, 1, "Expected exactly one obligation attached") + assert.Equal(t, createdObligationID, res.Data[0].ObligationIds[0], "Attached obligation ID does not match created obligation ID") + }) } func TestGetLicense(t *testing.T) { @@ -187,7 +256,7 @@ func TestUpdateLicense(t *testing.T) { Url: ptr("https://licenses.org/nonexistent"), TextUpdatable: ptr(false), Active: ptr(true), - SpdxId: ptr("NONEXISTENT"), + SpdxId: ptr("LicenseRef-NONEXISTENT"), } w := makeRequest("PATCH", "/licenses/"+uuid.New().String(), nonExistingLicense, true) assert.Equal(t, http.StatusNotFound, w.Code) @@ -202,4 +271,72 @@ func TestUpdateLicense(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.Status) }) + t.Run("UpdateLicenseErrorWithWrongObligations", func(t *testing.T) { + licenseWithObligation := models.LicenseUpdateDTO{ + ObligationIds: ptr([]uuid.UUID{uuid.New()}), + } + wLicense := makeRequest("PATCH", "/licenses"+id, licenseWithObligation, true) + assert.Equal(t, http.StatusNotFound, wLicense.Code, "Failed to throw error on license creation with wrong obligations") + }) + + t.Run("UpdateLicenseSuccessWithObligations", func(t *testing.T) { + // Create a dummy obligation first + dto := models.ObligationCreateDTO{ + Topic: "test-topic-license-update", + Type: "RIGHT", + Text: "some text", + Classification: "GREEN", + Comment: ptr("try to link obligations to a license in POST request"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + ExternalRef: models.ObligationSchemaExtension{ + ObligationExplanation: ptr("this is a test explaination to test the external ref functionality"), + }, + } + wObligation := makeRequest("POST", "/obligations", dto, true) + assert.Equal(t, http.StatusCreated, wObligation.Code, "Failed to create test obligation") + + var obligationRes models.ObligationResponse + err := json.Unmarshal(wObligation.Body.Bytes(), &obligationRes) + assert.NoError(t, err, "Failed to unmarshal obligation response") + assert.NotEmpty(t, obligationRes, "Obligation response data is empty") + createdObligationID := obligationRes.Data[0].Id + + licenseWithObligation := models.LicenseUpdateDTO{ + ObligationIds: ptr([]uuid.UUID{createdObligationID}), + } + // Update the license to link obligation + wLicense := makeRequest("PATCH", "/licenses/"+id, licenseWithObligation, true) + assert.Equal(t, http.StatusOK, wLicense.Code, "Failed to update license with obligations") + + var res models.LicenseResponse + if err := json.Unmarshal(wLicense.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling license response: %v", err) + return + } + if len(res.Data) == 0 { + t.Fatal("License response data is empty, cannot validate fields") + } + + assert.NotEmpty(t, res.Data[0].ObligationIds, "Expected obligations to be attached to the license") + assert.Len(t, res.Data[0].ObligationIds, 1, "Expected exactly one obligation attached") + assert.Equal(t, createdObligationID, res.Data[0].ObligationIds[0], "Attached obligation ID does not match created obligation ID") + + // Update the license to unlink obligation + licenseWithObligation = models.LicenseUpdateDTO{ + ObligationIds: ptr([]uuid.UUID{}), + } + // Update the license to link obligation + wLicense = makeRequest("PATCH", "/licenses/"+id, licenseWithObligation, true) + assert.Equal(t, http.StatusOK, wLicense.Code, "Failed to update license with obligations") + + if err := json.Unmarshal(wLicense.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling license response: %v", err) + return + } + if len(res.Data) == 0 { + t.Fatal("License response data is empty, cannot validate fields") + } + }) } diff --git a/tests/obligation_maps_test.go b/tests/obligation_maps_test.go deleted file mode 100644 index 1011b87..0000000 --- a/tests/obligation_maps_test.go +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Krishi Agrawal -// -// SPDX-License-Identifier: GPL-2.0-only - -package test - -import ( - "encoding/json" - "net/http" - "testing" - - "github.com/fossology/LicenseDb/pkg/models" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" -) - -func TestGetObligationMapByObligationId(t *testing.T) { - loginAs(t, "admin") - - licenseW := makeRequest("GET", "/licenses", nil, true) - assert.Equal(t, http.StatusOK, licenseW.Code) - - var licenseRes models.LicenseResponse - if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { - t.Fatalf("Failed to parse license response: %v", err) - } - - if len(licenseRes.Data) == 0 { - t.Skip("No licenses available for testing") - } - - dto := models.ObligationCreateDTO{ - Topic: "test-obligation-map", - Type: "RIGHT", - Text: "Test obligation text", - Classification: "GREEN", - Comment: ptr("Test comment"), - Active: ptr(true), - TextUpdatable: ptr(false), - Category: ptr("GENERAL"), - LicenseIds: []uuid.UUID{licenseRes.Data[0].Id}, - } - createW := makeRequest("POST", "/obligations", dto, true) - assert.Equal(t, http.StatusCreated, createW.Code) - - var createRes models.ObligationResponse - if err := json.Unmarshal(createW.Body.Bytes(), &createRes); err != nil { - t.Fatalf("Failed to parse create response: %v", err) - } - if len(createRes.Data) == 0 { - t.Fatal("No obligation returned in create response") - } - obligationId := createRes.Data[0].Id - - t.Run("getObligationMapByObligationId", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/obligation/"+obligationId.String(), nil, true) - assert.Equal(t, http.StatusOK, w.Code) - - var res models.ObligationMapResponse - if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { - t.Errorf("Error unmarshalling JSON: %v", err) - return - } - assert.Equal(t, http.StatusOK, res.Status) - if len(res.Data) > 0 { - assert.Equal(t, dto.Topic, res.Data[0].Topic) - assert.GreaterOrEqual(t, len(res.Data[0].Licenses), 0) - } - }) - - t.Run("getObligationMapForNonExistingObligation", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/obligation/"+uuid.New().String(), nil, true) - assert.Equal(t, http.StatusNotFound, w.Code) - }) - - t.Run("getObligationMapWithInvalidId", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/obligation/invalid-id", nil, true) - assert.Equal(t, http.StatusBadRequest, w.Code) - }) -} - -func TestGetObligationMapByLicenseId(t *testing.T) { - licenseW := makeRequest("GET", "/licenses", nil, true) - assert.Equal(t, http.StatusOK, licenseW.Code) - - var licenseRes models.LicenseResponse - if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { - t.Fatalf("Failed to parse license response: %v", err) - } - - if len(licenseRes.Data) == 0 { - t.Skip("No licenses available for testing") - } - licenseId := licenseRes.Data[0].Id - - t.Run("getObligationMapByLicenseId", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/license/"+licenseId.String(), nil, true) - assert.Equal(t, http.StatusOK, w.Code) - - var res models.ObligationMapResponse - if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { - t.Errorf("Error unmarshalling JSON: %v", err) - return - } - assert.Equal(t, http.StatusOK, res.Status) - assert.GreaterOrEqual(t, len(res.Data), 0) - }) - - t.Run("getObligationMapForNonExistingLicense", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/license/"+uuid.New().String(), nil, true) - assert.Equal(t, http.StatusNotFound, w.Code) - }) - - t.Run("getObligationMapWithInvalidId", func(t *testing.T) { - w := makeRequest("GET", "/obligation_maps/license/invalid-id", nil, true) - assert.Equal(t, http.StatusBadRequest, w.Code) - }) -} - -func TestPatchObligationMap(t *testing.T) { - dto := models.ObligationCreateDTO{ - Topic: "test-patch-obligation-map", - Type: "RIGHT", - Text: "Test obligation text", - Classification: "GREEN", - Comment: ptr("Test comment"), - Active: ptr(true), - TextUpdatable: ptr(false), - Category: ptr("GENERAL"), - } - createW := makeRequest("POST", "/obligations", dto, true) - assert.Equal(t, http.StatusCreated, createW.Code) - - var createRes models.ObligationResponse - if err := json.Unmarshal(createW.Body.Bytes(), &createRes); err != nil { - t.Fatalf("Failed to parse create response: %v", err) - } - if len(createRes.Data) == 0 { - t.Fatal("No obligation returned in create response") - } - obligationId := createRes.Data[0].Id - - licenseW := makeRequest("GET", "/licenses", nil, true) - assert.Equal(t, http.StatusOK, licenseW.Code) - - var licenseRes models.LicenseResponse - if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { - t.Fatalf("Failed to parse license response: %v", err) - } - - if len(licenseRes.Data) == 0 { - t.Skip("No licenses available for testing") - } - licenseId := licenseRes.Data[0].Id - - t.Run("patchObligationMapAddLicense", func(t *testing.T) { - mapInput := models.LicenseMapInput{ - MapInput: []models.LicenseMapElement{ - { - Id: licenseId, - Add: true, - }, - }, - } - - w := makeRequest("PATCH", "/obligation_maps/obligation/"+obligationId.String()+"/license", mapInput, true) - assert.Equal(t, http.StatusOK, w.Code) - - var res models.ObligationMapResponse - if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { - t.Errorf("Error unmarshalling JSON: %v", err) - return - } - assert.Equal(t, http.StatusOK, res.Status) - }) - - t.Run("patchObligationMapRemoveLicense", func(t *testing.T) { - mapInputAdd := models.LicenseMapInput{ - MapInput: []models.LicenseMapElement{ - { - Id: licenseId, - Add: true, - }, - }, - } - _ = makeRequest("PATCH", "/obligation_maps/obligation/"+obligationId.String()+"/license", mapInputAdd, true) - - mapInputRemove := models.LicenseMapInput{ - MapInput: []models.LicenseMapElement{ - { - Id: licenseId, - Add: false, - }, - }, - } - - w := makeRequest("PATCH", "/obligation_maps/obligation/"+obligationId.String()+"/license", mapInputRemove, true) - assert.Equal(t, http.StatusOK, w.Code) - }) - - t.Run("patchObligationMapForNonExistingObligation", func(t *testing.T) { - mapInput := models.LicenseMapInput{ - MapInput: []models.LicenseMapElement{ - { - Id: licenseId, - Add: true, - }, - }, - } - - w := makeRequest("PATCH", "/obligation_maps/obligation/"+uuid.New().String()+"/license", mapInput, true) - assert.Equal(t, http.StatusNotFound, w.Code) - }) - - t.Run("patchObligationMapWithInvalidJSON", func(t *testing.T) { - w := makeRequest("PATCH", "/obligation_maps/obligation/"+obligationId.String()+"/license", "invalid", true) - assert.Equal(t, http.StatusBadRequest, w.Code) - }) -} - -func TestUpdateLicenseInObligationMap(t *testing.T) { - dto := models.ObligationCreateDTO{ - Topic: "test-update-obligation-map", - Type: "RIGHT", - Text: "Test obligation text", - Classification: "GREEN", - Comment: ptr("Test comment"), - Active: ptr(true), - TextUpdatable: ptr(false), - Category: ptr("GENERAL"), - } - createW := makeRequest("POST", "/obligations", dto, true) - assert.Equal(t, http.StatusCreated, createW.Code) - - var createRes models.ObligationResponse - if err := json.Unmarshal(createW.Body.Bytes(), &createRes); err != nil { - t.Fatalf("Failed to parse create response: %v", err) - } - if len(createRes.Data) == 0 { - t.Fatal("No obligation returned in create response") - } - obligationId := createRes.Data[0].Id - - licenseW := makeRequest("GET", "/licenses", nil, true) - assert.Equal(t, http.StatusOK, licenseW.Code) - - var licenseRes models.LicenseResponse - if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { - t.Fatalf("Failed to parse license response: %v", err) - } - - if len(licenseRes.Data) == 0 { - t.Skip("No licenses available for testing") - } - licenseId := licenseRes.Data[0].Id - - t.Run("updateLicenseInObligationMap", func(t *testing.T) { - licenseListInput := models.LicenseListInput{ - LicenseIds: []uuid.UUID{licenseId}, - } - - w := makeRequest("PUT", "/obligation_maps/obligation/"+obligationId.String()+"/license", licenseListInput, true) - assert.Equal(t, http.StatusOK, w.Code) - - var res models.ObligationMapResponse - if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { - t.Errorf("Error unmarshalling JSON: %v", err) - return - } - assert.Equal(t, http.StatusOK, res.Status) - }) - - t.Run("updateLicenseInObligationMapWithEmptyList", func(t *testing.T) { - licenseListInput := models.LicenseListInput{ - LicenseIds: []uuid.UUID{}, - } - - w := makeRequest("PUT", "/obligation_maps/obligation/"+obligationId.String()+"/license", licenseListInput, true) - assert.Equal(t, http.StatusOK, w.Code) - }) - - t.Run("updateLicenseInObligationMapForNonExistingObligation", func(t *testing.T) { - licenseListInput := models.LicenseListInput{ - LicenseIds: []uuid.UUID{licenseId}, - } - - w := makeRequest("PUT", "/obligation_maps/obligation/"+uuid.New().String()+"/license", licenseListInput, true) - assert.Equal(t, http.StatusNotFound, w.Code) - }) - - t.Run("updateLicenseInObligationMapWithInvalidJSON", func(t *testing.T) { - w := makeRequest("PUT", "/obligation_maps/obligation/"+obligationId.String()+"/license", "invalid", true) - assert.Equal(t, http.StatusBadRequest, w.Code) - }) -} diff --git a/tests/obligations_comprehensive_test.go b/tests/obligations_comprehensive_test.go index 5732f5a..f029380 100644 --- a/tests/obligations_comprehensive_test.go +++ b/tests/obligations_comprehensive_test.go @@ -293,6 +293,140 @@ func TestImportObligations(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) }) + + t.Run("importSuccessUpdateExistingLicense", func(t *testing.T) { + licenseW := makeRequest("GET", "/licenses", nil, true) + assert.Equal(t, http.StatusOK, licenseW.Code) + + var licenseRes models.LicenseResponse + if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { + t.Fatalf("Failed to parse license response: %v", err) + } + + var licenseId *uuid.UUID + if len(licenseRes.Data) > 0 { + licenseId = &licenseRes.Data[0].Id + } + + obligations := []models.ObligationFileDTO{ + { + Id: ptr(uuid.New()), + Topic: ptr("IMPORT-OBLIGATION-1"), + Type: ptr("RIGHT"), + Text: ptr("Test obligation text for import"), + Classification: ptr("GREEN"), + Comment: ptr("Test comment"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + }, + } + + if licenseId != nil { + obligations[0].LicenseIds = &[]uuid.UUID{*licenseId} + } + + jsonData, err := json.Marshal(obligations) + assert.NoError(t, err) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "obligations.json") + assert.NoError(t, err) + _, err = part.Write(jsonData) + assert.NoError(t, err) + writer.Close() + + fullPath := baseURL + "/obligations/import" + req := httptest.NewRequest("POST", fullPath, body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+AuthToken) + w := httptest.NewRecorder() + api.Router().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var res models.ImportObligationsResponse + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling JSON: %v", err) + return + } + assert.Equal(t, http.StatusOK, res.Status) + }) + + t.Run("importSuccessUpdateExistingObligation", func(t *testing.T) { + licenseW := makeRequest("GET", "/licenses", nil, true) + assert.Equal(t, http.StatusOK, licenseW.Code) + + var licenseRes models.LicenseResponse + if err := json.Unmarshal(licenseW.Body.Bytes(), &licenseRes); err != nil { + t.Fatalf("Failed to parse license response: %v", err) + } + + var licenseId *uuid.UUID + if len(licenseRes.Data) > 0 { + licenseId = &licenseRes.Data[0].Id + } + + dto := models.ObligationCreateDTO{ + Topic: "test-import-topic", + Type: "RIGHT", + Text: "test text for import", + Classification: "GREEN", + Comment: ptr("unit test comment"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + ExternalRef: models.ObligationSchemaExtension{ + ObligationExplanation: ptr("this is a test explaination to test the external ref functionality"), + }, + LicenseIds: []uuid.UUID{*licenseId}, + } + id := assertObligationCreated(t, dto) + + licenseId = &licenseRes.Data[1].Id + obligations := []models.ObligationFileDTO{ + { + Id: ptr(id), + Topic: ptr("IMPORT-OBLIGATION-UPDATE"), + Type: ptr("RIGHT"), + Text: ptr("Test obligation text for import"), + Classification: ptr("GREEN"), + Comment: ptr("Test comment"), + Active: ptr(true), + TextUpdatable: ptr(false), + Category: ptr("GENERAL"), + LicenseIds: &[]uuid.UUID{*licenseId}, + }, + } + + jsonData, err := json.Marshal(obligations) + assert.NoError(t, err) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", "obligations.json") + assert.NoError(t, err) + _, err = part.Write(jsonData) + assert.NoError(t, err) + writer.Close() + + fullPath := baseURL + "/obligations/import" + req := httptest.NewRequest("POST", fullPath, body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+AuthToken) + w := httptest.NewRecorder() + api.Router().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var res models.ImportObligationsResponse + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling JSON: %v", err) + return + } + assert.Equal(t, http.StatusOK, res.Status) + }) } func TestGetSimilarObligations(t *testing.T) { diff --git a/tests/obligations_test.go b/tests/obligations_test.go index eaf654b..07a0285 100644 --- a/tests/obligations_test.go +++ b/tests/obligations_test.go @@ -76,7 +76,7 @@ func TestCreateObligation(t *testing.T) { } w := makeRequest("POST", "/obligations", dto, true) - assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Equal(t, http.StatusNotFound, w.Code) var res models.LicenseError if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { @@ -84,7 +84,7 @@ func TestCreateObligation(t *testing.T) { return } - expectedError := "Obligation created successfully but license association failed" + expectedError := "Failed to create obligation" assert.Equal(t, expectedError, res.Message) }) @@ -199,6 +199,50 @@ func TestUpdateObligation(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.Status) }) + t.Run("UpdateLicenseAssociations", func(t *testing.T) { + w := makeRequest("GET", "/licenses", nil, true) + assert.Equal(t, http.StatusOK, w.Code) + + var res models.LicenseResponse + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling JSON: %v", err) + return + } + + updateDTO := models.ObligationUpdateDTO{ + LicenseIds: ptr([]uuid.UUID{res.Data[0].Id, res.Data[1].Id}), + } + + assertObligationUpdated(t, id, updateDTO) + + updateDTO = models.ObligationUpdateDTO{ + LicenseIds: ptr([]uuid.UUID{res.Data[2].Id}), + } + + assertObligationUpdated(t, id, updateDTO) + + updateDTO = models.ObligationUpdateDTO{ + LicenseIds: ptr([]uuid.UUID{}), + } + + assertObligationUpdated(t, id, updateDTO) + }) + + t.Run("UpdateLicenseAssociationsWithNonExistentLicense", func(t *testing.T) { + updateDTO := models.ObligationUpdateDTO{ + LicenseIds: ptr([]uuid.UUID{uuid.New()}), + } + + w := makeRequest("PATCH", "/obligations/"+id.String(), updateDTO, true) + + var res models.LicenseError + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Errorf("Error unmarshalling JSON: %v", err) + return + } + + assert.Equal(t, http.StatusBadRequest, res.Status) + }) } func TestDeleteObligation(t *testing.T) {