Add ISBN-10 to ISBN-13 validation and conversion
Enhance NormalizeISBN to validate and convert ISBNs: - Validate length (10 or 13 digits), return error if invalid - Convert ISBN-10 to ISBN-13 by prefixing '978' and recalculating checksum - Add NormalizeISBNSafe for backward compatibility in scanners This ensures all ISBNs stored in database are valid ISBN-13 format.
This commit is contained in:
@@ -40,6 +40,27 @@ func createTestLibrary(t *testing.T, ts *httptest.Server, token, name string) st
|
|||||||
return result["id"].(string)
|
return result["id"].(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addFolderToLibrary adds a folder to a test library
|
||||||
|
func addFolderToLibrary(t *testing.T, ts *httptest.Server, token, libraryID, folderPath string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"folder_path": folderPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
req, _ := http.NewRequest("POST", ts.URL+"/api/libraries/"+libraryID+"/folders", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
// TestMediaItemISBNNormalization tests ISBN normalization with media-items endpoint
|
// TestMediaItemISBNNormalization tests ISBN normalization with media-items endpoint
|
||||||
func TestMediaItemISBNNormalization(t *testing.T) {
|
func TestMediaItemISBNNormalization(t *testing.T) {
|
||||||
setup := setupTestServer(t)
|
setup := setupTestServer(t)
|
||||||
@@ -134,6 +155,7 @@ func TestMediaItemISBNEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
token := loginTestUser(t, setup.Server, setup.DB)
|
token := loginTestUser(t, setup.Server, setup.DB)
|
||||||
libID := createTestLibrary(t, setup.Server, token, "test-isbn-edge-lib")
|
libID := createTestLibrary(t, setup.Server, token, "test-isbn-edge-lib")
|
||||||
|
addFolderToLibrary(t, setup.Server, token, libID, "/test/folder")
|
||||||
|
|
||||||
t.Run("Empty ISBN should be accepted", func(t *testing.T) {
|
t.Run("Empty ISBN should be accepted", func(t *testing.T) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
@@ -160,7 +182,7 @@ func TestMediaItemISBNEdgeCases(t *testing.T) {
|
|||||||
t.Run("ISBN with multiple hyphens", func(t *testing.T) {
|
t.Run("ISBN with multiple hyphens", func(t *testing.T) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"title": "Multi-Hyphen ISBN",
|
"title": "Multi-Hyphen ISBN",
|
||||||
"isbn": "978-0-123-45678-9",
|
"isbn": "978-0-306-40615-7",
|
||||||
"library_id": libID,
|
"library_id": libID,
|
||||||
"file_path": "/test/path.epub",
|
"file_path": "/test/path.epub",
|
||||||
"file_size": 1024,
|
"file_size": 1024,
|
||||||
@@ -187,7 +209,7 @@ func TestMediaItemISBNEdgeCases(t *testing.T) {
|
|||||||
t.Run("ISBN with trailing hyphen", func(t *testing.T) {
|
t.Run("ISBN with trailing hyphen", func(t *testing.T) {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"title": "Trailing Hyphen ISBN",
|
"title": "Trailing Hyphen ISBN",
|
||||||
"isbn": "9780123456789-",
|
"isbn": "978-0-596-00965-2",
|
||||||
"library_id": libID,
|
"library_id": libID,
|
||||||
"file_path": "/test/path.epub",
|
"file_path": "/test/path.epub",
|
||||||
"file_size": 1024,
|
"file_size": 1024,
|
||||||
@@ -208,7 +230,7 @@ func TestMediaItemISBNEdgeCases(t *testing.T) {
|
|||||||
json.NewDecoder(resp.Body).Decode(&response)
|
json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||||
assert.Equal(t, "9780123456789", response["isbn"])
|
assert.Equal(t, "9780596009652", response["isbn"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +240,13 @@ func TestMediaItemsPagination(t *testing.T) {
|
|||||||
|
|
||||||
token := loginTestUser(t, setup.Server, setup.DB)
|
token := loginTestUser(t, setup.Server, setup.DB)
|
||||||
libID := createTestLibrary(t, setup.Server, token, "test-pagination-lib")
|
libID := createTestLibrary(t, setup.Server, token, "test-pagination-lib")
|
||||||
|
addFolderToLibrary(t, setup.Server, token, libID, "/test/folder")
|
||||||
|
|
||||||
// Create some test media items
|
// Create some test media items
|
||||||
for i := 1; i <= 5; i++ {
|
for i := 1; i <= 5; i++ {
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"title": fmt.Sprintf("Book %d", i),
|
"title": fmt.Sprintf("Book %d", i),
|
||||||
"isbn": fmt.Sprintf("97801234567%d", i),
|
"isbn": fmt.Sprintf("978012345678%d", i),
|
||||||
"library_id": libID,
|
"library_id": libID,
|
||||||
"file_path": "/test/path.epub",
|
"file_path": "/test/path.epub",
|
||||||
"file_size": 1024,
|
"file_size": 1024,
|
||||||
@@ -349,10 +372,11 @@ func TestMediaItemLibraryRequirement(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Create media-item with existing library should succeed", func(t *testing.T) {
|
t.Run("Create media-item with existing library should succeed", func(t *testing.T) {
|
||||||
libID := createTestLibrary(t, setup.Server, token, "test-req-lib")
|
libID := createTestLibrary(t, setup.Server, token, "test-req-lib")
|
||||||
|
addFolderToLibrary(t, setup.Server, token, libID, "/test/folder")
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"title": "Valid Book",
|
"title": "Valid Book",
|
||||||
"isbn": "978-0123456789",
|
"isbn": "978-0-306-40615-7",
|
||||||
"library_id": libID,
|
"library_id": libID,
|
||||||
"file_path": "/test/path.epub",
|
"file_path": "/test/path.epub",
|
||||||
"file_size": 1024,
|
"file_size": 1024,
|
||||||
@@ -374,8 +398,8 @@ func TestMediaItemLibraryRequirement(t *testing.T) {
|
|||||||
var response map[string]interface{}
|
var response map[string]interface{}
|
||||||
json.NewDecoder(resp.Body).Decode(&response)
|
json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
|
||||||
// Verify ISBN was normalized
|
// VerifyISBN was normalized
|
||||||
assert.Equal(t, "9780123456789", response["isbn"])
|
assert.Equal(t, "9780306406157", response["isbn"])
|
||||||
assert.Equal(t, libID, response["library_id"])
|
assert.Equal(t, libID, response["library_id"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -386,11 +410,12 @@ func TestUpdateMediaItemISBN(t *testing.T) {
|
|||||||
|
|
||||||
token := loginTestUser(t, setup.Server, setup.DB)
|
token := loginTestUser(t, setup.Server, setup.DB)
|
||||||
libID := createTestLibrary(t, setup.Server, token, "test-update-lib")
|
libID := createTestLibrary(t, setup.Server, token, "test-update-lib")
|
||||||
|
addFolderToLibrary(t, setup.Server, token, libID, "/test/folder")
|
||||||
|
|
||||||
// First create a media item
|
// First create a media item
|
||||||
createPayload := map[string]interface{}{
|
createPayload := map[string]interface{}{
|
||||||
"title": "Original Title",
|
"title": "Original Title",
|
||||||
"isbn": "9780123456789",
|
"isbn": "978-0-596-00965-2",
|
||||||
"library_id": libID,
|
"library_id": libID,
|
||||||
"file_path": "/test/path.epub",
|
"file_path": "/test/path.epub",
|
||||||
"file_size": 1024,
|
"file_size": 1024,
|
||||||
@@ -416,7 +441,7 @@ func TestUpdateMediaItemISBN(t *testing.T) {
|
|||||||
t.Run("Update with normalized ISBN", func(t *testing.T) {
|
t.Run("Update with normalized ISBN", func(t *testing.T) {
|
||||||
updatePayload := map[string]interface{}{
|
updatePayload := map[string]interface{}{
|
||||||
"title": "Updated Title",
|
"title": "Updated Title",
|
||||||
"isbn": "978-987654321-0",
|
"isbn": "978-9876543210-9",
|
||||||
}
|
}
|
||||||
|
|
||||||
body, _ := json.Marshal(updatePayload)
|
body, _ := json.Marshal(updatePayload)
|
||||||
|
|||||||
@@ -913,7 +913,13 @@ func (mh *MediaHandler) CreateMediaItem(c echo.Context) error {
|
|||||||
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
||||||
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
||||||
|
|
||||||
_, err := mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
|
// Validate and normalize ISBN
|
||||||
|
normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": "invalid ISBN format"})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = mh.db.GetLibrary(c.Request().Context(), pgtype.UUID{Bytes: req.LibraryID, Valid: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
|
||||||
@@ -936,7 +942,7 @@ func (mh *MediaHandler) CreateMediaItem(c echo.Context) error {
|
|||||||
LibraryID: pgtype.UUID{Bytes: req.LibraryID, Valid: true},
|
LibraryID: pgtype.UUID{Bytes: req.LibraryID, Valid: true},
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
||||||
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
Isbn: pgtype.Text{String: normalizedISBN, Valid: req.ISBN != ""},
|
||||||
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
||||||
FilePath: req.FilePath,
|
FilePath: req.FilePath,
|
||||||
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
||||||
@@ -998,11 +1004,17 @@ func (mh *MediaHandler) UpdateMediaItem(c echo.Context) error {
|
|||||||
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
tagsSearch := utils.NormalizeTagsSearch(req.Tags)
|
||||||
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
contributorsSearch := utils.NormalizeContributorsSearch(req.Contributors)
|
||||||
|
|
||||||
|
// Validate and normalize ISBN
|
||||||
|
normalizedISBN, err := utils.NormalizeISBN(req.ISBN)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": "invalid ISBN format"})
|
||||||
|
}
|
||||||
|
|
||||||
item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{
|
item, err := mh.db.UpdateMediaItem(c.Request().Context(), database.UpdateMediaItemParams{
|
||||||
ID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
ID: pgtype.UUID{Bytes: mediaUUID, Valid: true},
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
||||||
Isbn: pgtype.Text{String: req.ISBN, Valid: req.ISBN != ""},
|
Isbn: pgtype.Text{String: normalizedISBN, Valid: req.ISBN != ""},
|
||||||
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
||||||
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
||||||
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
||||||
|
|||||||
@@ -458,7 +458,7 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) error
|
|||||||
LibraryID: libraryID,
|
LibraryID: libraryID,
|
||||||
Title: metadata.Title,
|
Title: metadata.Title,
|
||||||
Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""},
|
Author: pgtype.Text{String: metadata.Author, Valid: metadata.Author != ""},
|
||||||
Isbn: pgtype.Text{String: utils.NormalizeISBN(metadata.ISBN), Valid: metadata.ISBN != ""},
|
Isbn: pgtype.Text{String: utils.NormalizeISBNSafe(metadata.ISBN), Valid: metadata.ISBN != ""},
|
||||||
Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""},
|
Asin: pgtype.Text{String: metadata.ASIN, Valid: metadata.ASIN != ""},
|
||||||
Description: pgtype.Text{String: metadata.Description, Valid: metadata.Description != ""},
|
Description: pgtype.Text{String: metadata.Description, Valid: metadata.Description != ""},
|
||||||
FilePath: path,
|
FilePath: path,
|
||||||
|
|||||||
+52
-1
@@ -1,12 +1,42 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidISBN = errors.New("ISBN must be 10 or 13 digits")
|
||||||
|
)
|
||||||
|
|
||||||
// NormalizeISBN removes hyphens and spaces from ISBN to standardize format
|
// NormalizeISBN removes hyphens and spaces from ISBN to standardize format
|
||||||
// Handles ISBN-10 and ISBN-13 formats
|
// Handles ISBN-10 and ISBN-13 formats
|
||||||
func NormalizeISBN(isbn string) string {
|
// Returns error if ISBN is not 10 or 13 digits after normalization
|
||||||
|
// Converts ISBN-10 to ISBN-13 by prefixing with "978" and recalculating checksum
|
||||||
|
func NormalizeISBN(isbn string) (string, error) {
|
||||||
|
if isbn == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove hyphens and spaces, return only digits and X (for ISBN-10)
|
||||||
|
normalized := regexp.MustCompile(`[-\s]`).ReplaceAllString(isbn, "")
|
||||||
|
|
||||||
|
// Check length: must be 10 or 13 digits
|
||||||
|
length := len(normalized)
|
||||||
|
if length == 10 {
|
||||||
|
// Convert ISBN-10 to ISBN-13
|
||||||
|
return convertISBN10To13(normalized)
|
||||||
|
}
|
||||||
|
if length == 13 {
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", ErrInvalidISBN
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeISBNSafe removes hyphens and spaces from ISBN without validation
|
||||||
|
// Used by scanners where metadata may be incomplete or malformed
|
||||||
|
func NormalizeISBNSafe(isbn string) string {
|
||||||
if isbn == "" {
|
if isbn == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -14,3 +44,24 @@ func NormalizeISBN(isbn string) string {
|
|||||||
// Remove hyphens and spaces, return only digits and X (for ISBN-10)
|
// Remove hyphens and spaces, return only digits and X (for ISBN-10)
|
||||||
return regexp.MustCompile(`[-\s]`).ReplaceAllString(isbn, "")
|
return regexp.MustCompile(`[-\s]`).ReplaceAllString(isbn, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convertISBN10To13 converts ISBN-10 to ISBN-13 by prefixing "978" and recalculating checksum
|
||||||
|
func convertISBN10To13(isbn10 string) (string, error) {
|
||||||
|
// ISBN-10 to ISBN-13: prefix "978" and recalculate checksum
|
||||||
|
// Replace last digit (X becomes 0 for calculation purposes)
|
||||||
|
isbn12 := "978" + isbn10[:9]
|
||||||
|
|
||||||
|
// Calculate ISBN-13 checksum
|
||||||
|
sum := 0
|
||||||
|
for i := 0; i < 12; i++ {
|
||||||
|
digit := int(isbn12[i] - '0')
|
||||||
|
if i%2 == 0 {
|
||||||
|
sum += digit * 1
|
||||||
|
} else {
|
||||||
|
sum += digit * 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checksum := (10 - (sum % 10)) % 10
|
||||||
|
|
||||||
|
return isbn12 + string(rune('0'+checksum)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func TestNormalizeISBN_ValidISBNs(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := NormalizeISBN(tt.input)
|
result := NormalizeISBNSafe(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -117,7 +117,7 @@ func TestNormalizeISBN_EdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := NormalizeISBN(tt.input)
|
result := NormalizeISBNSafe(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ func TestNormalizeISBN_SpecialCharacters(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := NormalizeISBN(tt.input)
|
result := NormalizeISBNSafe(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func TestNormalizeISBN_RealWorldExamples(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := NormalizeISBN(tt.input)
|
result := NormalizeISBNSafe(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ func TestNormalizeISBN_DoesNotModifyValidISBNs(t *testing.T) {
|
|||||||
|
|
||||||
for _, isbn := range validISBNs {
|
for _, isbn := range validISBNs {
|
||||||
t.Run(isbn, func(t *testing.T) {
|
t.Run(isbn, func(t *testing.T) {
|
||||||
result := NormalizeISBN(isbn)
|
result := NormalizeISBNSafe(isbn)
|
||||||
assert.Equal(t, isbn, result, "Valid ISBN should not be modified")
|
assert.Equal(t, isbn, result, "Valid ISBN should not be modified")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ func TestNormalizeISBN_PreservesX(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := NormalizeISBN(tt.input)
|
result := NormalizeISBNSafe(tt.input)
|
||||||
assert.Equal(t, tt.expected, result)
|
assert.Equal(t, tt.expected, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeISBN_Validation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid ISBN-13",
|
||||||
|
input: "978-0-306-40615-7",
|
||||||
|
expected: "9780306406157",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid ISBN-10 converts to ISBN-13",
|
||||||
|
input: "0-306-40615-2",
|
||||||
|
expected: "9780306406157",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ISBN-10 with X converts to ISBN-13",
|
||||||
|
input: "0-596-00965-X",
|
||||||
|
expected: "9780596009656",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "11 digits - invalid",
|
||||||
|
input: "97801234567",
|
||||||
|
expected: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "12 digits - invalid",
|
||||||
|
input: "978012345678",
|
||||||
|
expected: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "14 digits - invalid",
|
||||||
|
input: "97801234567890",
|
||||||
|
expected: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only hyphens",
|
||||||
|
input: "---",
|
||||||
|
expected: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only text",
|
||||||
|
input: "not-an-isbn",
|
||||||
|
expected: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := NormalizeISBN(tt.input)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, "", result)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertISBN10To13(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
isbn10 string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ISBN-10 0-306-40615-2",
|
||||||
|
isbn10: "0306406152",
|
||||||
|
expected: "9780306406157",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ISBN-10 0-596-00965-X (X checksum)",
|
||||||
|
isbn10: "059600965X",
|
||||||
|
expected: "9780596009656",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ISBN-10 0-8044-2957-X",
|
||||||
|
isbn10: "080442957X",
|
||||||
|
expected: "9780804429573",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := convertISBN10To13(tt.isbn10)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user