diff --git a/cmd/laas/docs/docs.go b/cmd/laas/docs/docs.go index 764cb00..2fea821 100644 --- a/cmd/laas/docs/docs.go +++ b/cmd/laas/docs/docs.go @@ -2490,12 +2490,6 @@ const docTemplate = `{ } }, "definitions": { - "datatypes.JSONType-models_LicenseDBSchemaExtension": { - "type": "object" - }, - "datatypes.JSONType-models_ObligationSchemaExtension": { - "type": "object" - }, "models.APICollection": { "type": "object", "properties": { @@ -2746,7 +2740,7 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -2788,68 +2782,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": { @@ -3017,7 +2949,7 @@ const docTemplate = `{ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -3078,10 +3010,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 +3048,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": [ diff --git a/cmd/laas/docs/swagger.json b/cmd/laas/docs/swagger.json index 9894923..9221c0a 100644 --- a/cmd/laas/docs/swagger.json +++ b/cmd/laas/docs/swagger.json @@ -2483,12 +2483,6 @@ } }, "definitions": { - "datatypes.JSONType-models_LicenseDBSchemaExtension": { - "type": "object" - }, - "datatypes.JSONType-models_ObligationSchemaExtension": { - "type": "object" - }, "models.APICollection": { "type": "object", "properties": { @@ -2739,7 +2733,7 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -2781,68 +2775,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": { @@ -3010,7 +2942,7 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { "type": "string" @@ -3071,10 +3003,10 @@ "type": "string", "example": "This license has been superseded." }, - "obligations": { + "obligation_ids": { "type": "array", "items": { - "$ref": "#/definitions/models.Obligation" + "type": "string" } }, "risk": { @@ -3109,62 +3041,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": [ diff --git a/cmd/laas/docs/swagger.yaml b/cmd/laas/docs/swagger.yaml index 7928f6e..89fa8ac 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: @@ -365,7 +320,7 @@ definitions: notes: example: This license has been superseded. type: string - obligations: + obligation_ids: items: type: string type: array @@ -409,9 +364,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 +392,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: 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/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/utils/util.go b/pkg/utils/util.go index f01be78..4707cf3 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,154 @@ 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) + } + + // for setting api response + lic.Id = &license.Id + lic.Shortname = license.Shortname + + if importStatus == 0 { + 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 := query.Clauses(clause.Returning{}).Updates(&newLicense).Scan(&newLicense).Error; err != nil { - message = fmt.Sprintf("failed to update license: %s", err.Error()) + 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 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 +287,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 }) @@ -350,6 +446,28 @@ func createObligationMapChangelog( return nil } +// 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 + + 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.Debug().Model(license).Association("Obligations").Replace(newObligationAssociations); err != nil { + errs = append(errs, err) + } + + return errs +} + func AddChangelogForObligationType(tx *gorm.DB, userId uuid.UUID, oldObType, newObType *models.ObligationType) error { var changes []models.ChangeLog AddChangelog("Active", oldObType.Active, newObType.Active, &changes) @@ -712,6 +830,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 +855,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..7a6ca47 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,274 @@ 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) + }) + + t.Run("importNewLicenseWithId", func(t *testing.T) { + // Create a dummy obligation first + dto := models.ObligationCreateDTO{ + Topic: "test-topic-license-import-id", + 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{ + { + Id: ptr(uuid.New()), + Shortname: ptr("IMPORT-TEST-OBL-ID"), + 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) + }) + + t.Run("importUpdateExistingLicense", func(t *testing.T) { + license := models.LicenseCreateDTO{ + Shortname: "MIT1-UpdateExisting", + Fullname: "MIT License", + 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-MIT1-UpdateExisting", + Risk: ptr(int64(2)), + } + wLicense := makeRequest("POST", "/licenses", license, true) + assert.Equal(t, http.StatusCreated, wLicense.Code) + var licenseRes models.LicenseResponse + if err := json.Unmarshal(wLicense.Body.Bytes(), &licenseRes); err != nil { + t.Errorf("Error unmarshalling response: %v", err) + return + } + if len(licenseRes.Data) == 0 { + t.Fatal("Response data is empty, cannot validate fields") + } + createdLicenseID := licenseRes.Data[0].Id + + dto := models.ObligationCreateDTO{ + Topic: "test-topic-license-import-existing", + 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{ + { + Id: ptr(createdLicenseID), + Shortname: ptr("IMPORT-TEST-OBL-ID"), + 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) + }) + + t.Run("importLicenseWithNonExistentObligationAssociation", func(t *testing.T) { + licenses := []models.LicenseImportDTO{ + { + Shortname: ptr("IMPORT-TEST-OBL-ID-NON-LICENSE"), + 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{uuid.New()}), + }, + } + + 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") + } + }) }