Skip to content

Commit 416e7ee

Browse files
fix: enforce minimum one obligation requirement for licenses (Issue fossology#136)
- Add validation to CreateLicense endpoint to reject licenses without obligations - Add validation to UpdateLicense endpoint to prevent removing all obligations - Add validation to ImportLicenses flow for imported licenses - Update existing tests to include obligations - Add new test case for license creation without obligations - Create getTestObligation() helper function for tests Addresses all feedback from PR fossology#191: - Validation occurs before database transactions (not after) - UpdateLicense endpoint now validates obligations - All test cases updated and passing - Clear, user-friendly error messages Fixes fossology#136 Signed-off-by: abhayrajjais01 <abhayraj916146@gmail.com>
1 parent 03d0b09 commit 416e7ee

File tree

4 files changed

+118
-23
lines changed

4 files changed

+118
-23
lines changed

pkg/api/licenses.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,19 @@ func CreateLicense(c *gin.Context) {
252252
return
253253
}
254254

255+
// Validate that at least one obligation is attached
256+
if input.Obligations == nil || len(*input.Obligations) == 0 {
257+
er := models.LicenseError{
258+
Status: http.StatusBadRequest,
259+
Message: "cannot create license without obligations",
260+
Error: "at least one obligation must be attached to the license",
261+
Path: c.Request.URL.Path,
262+
Timestamp: time.Now().Format(time.RFC3339),
263+
}
264+
c.JSON(http.StatusBadRequest, er)
265+
return
266+
}
267+
255268
lic := input.ConvertToLicenseDB()
256269

257270
lic.UserId = userId
@@ -399,6 +412,19 @@ func UpdateLicense(c *gin.Context) {
399412
return errors.New("field `text_updatable` needs to be true to update the text")
400413
}
401414

415+
// Validate that obligations are not being set to empty
416+
if updates.Obligations != nil && len(*updates.Obligations) == 0 {
417+
er := models.LicenseError{
418+
Status: http.StatusBadRequest,
419+
Message: "cannot update license to have zero obligations",
420+
Error: "at least one obligation must remain attached to the license",
421+
Path: c.Request.URL.Path,
422+
Timestamp: time.Now().Format(time.RFC3339),
423+
}
424+
c.JSON(http.StatusBadRequest, er)
425+
return errors.New("cannot update license to have zero obligations")
426+
}
427+
402428
// Overwrite values of existing keys, add new key value pairs and remove keys with null values.
403429
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 {
404430
er := models.LicenseError{

pkg/utils/util.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ func InsertOrUpdateLicenseOnImport(lic *models.LicenseImportDTO, userId uuid.UUI
124124
return message, importStatus
125125
}
126126

127+
// Validate that at least one obligation is attached
128+
if lic.Obligations == nil || len(*lic.Obligations) == 0 {
129+
message = "cannot import license without obligations: at least one obligation is required"
130+
importStatus = IMPORT_FAILED
131+
return message, importStatus
132+
}
133+
127134
_ = db.DB.Transaction(func(tx *gorm.DB) error {
128135
license := lic.ConvertToLicenseDB()
129136
license.UserId = userId

tests/licenses_test.go

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ import (
1616

1717
func TestCreateLicense(t *testing.T) {
1818
license := models.LicenseCreateDTO{
19-
Shortname: "MIT1",
20-
Fullname: "MIT License",
21-
Text: `MIT1 License copyright (c) <year> <copyright holders> 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.`,
22-
Url: ptr("https://opensource.org/licenses/MIT"),
23-
Notes: ptr("This license is OSI approved."),
24-
Source: ptr("spdx"),
25-
SpdxId: "LicenseRef-MIT1",
26-
Risk: ptr(int64(2)),
19+
Shortname: "MIT1",
20+
Fullname: "MIT License",
21+
Text: `MIT1 License copyright (c) <year> <copyright holders> 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.`,
22+
Url: ptr("https://opensource.org/licenses/MIT"),
23+
Notes: ptr("This license is OSI approved."),
24+
Source: ptr("spdx"),
25+
SpdxId: "LicenseRef-MIT1",
26+
Risk: ptr(int64(2)),
27+
Obligations: &[]uuid.UUID{getTestObligation()},
2728
}
2829

2930
t.Run("success", func(t *testing.T) {
@@ -57,17 +58,39 @@ func TestCreateLicense(t *testing.T) {
5758
})
5859
t.Run("unauthorized", func(t *testing.T) {
5960
license := models.LicenseCreateDTO{
60-
Shortname: "UnauthorizedLicense",
61-
Fullname: "Unauthorized License",
62-
Text: "This license should not be created without authentication.",
63-
Url: ptr("https://licenses.org/unauthorized"),
64-
SpdxId: "UNAUTHORIZED",
65-
Notes: ptr("This license is OSI approved."),
66-
Risk: ptr(int64(2)),
61+
Shortname: "UnauthorizedLicense",
62+
Fullname: "Unauthorized License",
63+
Text: "This license should not be created without authentication.",
64+
Url: ptr("https://licenses.org/unauthorized"),
65+
SpdxId: "UNAUTHORIZED",
66+
Notes: ptr("This license is OSI approved."),
67+
Risk: ptr(int64(2)),
68+
Obligations: &[]uuid.UUID{getTestObligation()},
6769
}
6870
w := makeRequest("POST", "/licenses", license, false)
6971
assert.Equal(t, http.StatusUnauthorized, w.Code)
7072
})
73+
t.Run("withoutObligations", func(t *testing.T) {
74+
licenseWithoutObligations := models.LicenseCreateDTO{
75+
Shortname: "NoObligLicense",
76+
Fullname: "License Without Obligations",
77+
Text: "This license should not be created without obligations.",
78+
Url: ptr("https://licenses.org/nooblig"),
79+
SpdxId: "LicenseRef-NoOblig",
80+
Notes: ptr("Test for obligation validation."),
81+
Risk: ptr(int64(2)),
82+
// Obligations intentionally omitted
83+
}
84+
w := makeRequest("POST", "/licenses", licenseWithoutObligations, true)
85+
assert.Equal(t, http.StatusBadRequest, w.Code)
86+
87+
var res models.LicenseError
88+
if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil {
89+
t.Errorf("Error unmarshalling response: %v", err)
90+
return
91+
}
92+
assert.Contains(t, res.Message, "without obligations")
93+
})
7194
}
7295

7396
func TestGetLicense(t *testing.T) {
@@ -116,14 +139,15 @@ func TestGetLicense(t *testing.T) {
116139

117140
func TestUpdateLicense(t *testing.T) {
118141
license := models.LicenseCreateDTO{
119-
Shortname: "MIT2",
120-
Fullname: "MIT License 2",
121-
Text: `MIT1 License copyright text`,
122-
Url: ptr("https://opensource.org/licenses/MIT"),
123-
Notes: ptr("This license is OSI approved."),
124-
Source: ptr("spdx"),
125-
SpdxId: "LicenseRef-MIT2",
126-
Risk: ptr(int64(2)),
142+
Shortname: "MIT2",
143+
Fullname: "MIT License 2",
144+
Text: `MIT1 License copyright text`,
145+
Url: ptr("https://opensource.org/licenses/MIT"),
146+
Notes: ptr("This license is OSI approved."),
147+
Source: ptr("spdx"),
148+
SpdxId: "LicenseRef-MIT2",
149+
Risk: ptr(int64(2)),
150+
Obligations: &[]uuid.UUID{getTestObligation()},
127151
}
128152
w := makeRequest("POST", "/licenses", license, true)
129153
var res models.LicenseResponse

tests/main_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/golang-migrate/migrate/v4"
2424
_ "github.com/golang-migrate/migrate/v4/database/postgres"
2525
_ "github.com/golang-migrate/migrate/v4/source/file"
26+
"github.com/google/uuid"
2627
"github.com/joho/godotenv"
2728
_ "github.com/lib/pq"
2829
"go.uber.org/zap"
@@ -93,6 +94,43 @@ func ptr[T any](v T) *T {
9394
return &v
9495
}
9596

97+
// getTestObligation returns a test obligation ID, creating one if it doesn't exist
98+
func getTestObligation() uuid.UUID {
99+
var obligation models.Obligation
100+
// Try to find an existing obligation
101+
if err := db.DB.First(&obligation).Error; err == nil {
102+
return obligation.Id
103+
}
104+
105+
// If no obligation exists, create one for testing
106+
var obType models.ObligationType
107+
var obClassification models.ObligationClassification
108+
109+
// Get or create obligation type
110+
db.DB.Where(models.ObligationType{Type: "OBLIGATION"}).FirstOrCreate(&obType, models.ObligationType{
111+
Type: "OBLIGATION",
112+
Active: ptr(true),
113+
})
114+
115+
// Get or create obligation classification
116+
db.DB.Where(models.ObligationClassification{Classification: "GREEN"}).FirstOrCreate(&obClassification, models.ObligationClassification{
117+
Classification: "GREEN",
118+
Color: "#00FF00",
119+
Active: ptr(true),
120+
})
121+
122+
// Create test obligation
123+
obligation = models.Obligation{
124+
Topic: ptr("Test Obligation"),
125+
Text: ptr("This is a test obligation for license testing"),
126+
Active: ptr(true),
127+
ObligationTypeId: obType.Id,
128+
ObligationClassificationId: obClassification.Id,
129+
}
130+
db.DB.Create(&obligation)
131+
return obligation.Id
132+
}
133+
96134
// utility functions
97135

98136
func createTestDB(user, password, port, host, dbname string) {

0 commit comments

Comments
 (0)