Skip to content

Commit 49800af

Browse files
authored
refactor: improve tokenization module (#102)
- Add `ErrTokenizationKeyCreatedAtInvalid` and update `TokenizationKey.Validate()` to use it for zero creation timestamps. - Refactor `Rotate()` use case method to reuse `createTokenizationKey` helper, reducing code duplication. - Remove unused offset/limit-based `List` repository methods from both PostgreSQL and MySQL repositories. - Add comprehensive integration tests for `Get` and `GetByNameAndVersion` repository methods in MySQL and PostgreSQL. - Improve docstrings for `Rotate`, `Delete`, `ListCursor`, and `PurgeDeleted` use case methods.
1 parent fa206cf commit 49800af

7 files changed

Lines changed: 206 additions & 193 deletions

File tree

internal/tokenization/domain/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,10 @@ var (
5959
errors.ErrInvalidInput,
6060
"tokenization key DEK ID cannot be nil",
6161
)
62+
63+
// ErrTokenizationKeyCreatedAtInvalid indicates the creation timestamp is invalid (zero time).
64+
ErrTokenizationKeyCreatedAtInvalid = errors.Wrap(
65+
errors.ErrInvalidInput,
66+
"tokenization key creation timestamp cannot be zero",
67+
)
6268
)

internal/tokenization/domain/tokenization_key.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (tk *TokenizationKey) Validate() error {
5959
return ErrTokenizationKeyDekIDInvalid
6060
}
6161
if tk.CreatedAt.IsZero() {
62-
return ErrInvalidFormatType // Using existing error for now
62+
return ErrTokenizationKeyCreatedAtInvalid
6363
}
6464
return nil
6565
}

internal/tokenization/repository/mysql/mysql_repository.go

Lines changed: 0 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -221,79 +221,6 @@ func (m *MySQLTokenizationKeyRepository) GetByNameAndVersion(
221221
return &key, nil
222222
}
223223

224-
// List retrieves tokenization keys ordered by name ascending with pagination.
225-
// Returns the latest version for each key.
226-
func (m *MySQLTokenizationKeyRepository) List(
227-
ctx context.Context,
228-
offset, limit int,
229-
) ([]*tokenizationDomain.TokenizationKey, error) {
230-
querier := database.GetTx(ctx, m.db)
231-
232-
query := `
233-
SELECT tk.id, tk.name, tk.version, tk.format_type, tk.is_deterministic, tk.salt, tk.dek_id, tk.created_at, tk.deleted_at
234-
FROM tokenization_keys tk
235-
INNER JOIN (
236-
SELECT name, MAX(version) as max_version
237-
FROM tokenization_keys
238-
WHERE deleted_at IS NULL
239-
GROUP BY name
240-
ORDER BY name ASC
241-
LIMIT ? OFFSET ?
242-
) latest ON tk.name = latest.name AND tk.version = latest.max_version
243-
ORDER BY tk.name ASC`
244-
245-
rows, err := querier.QueryContext(ctx, query, limit, offset)
246-
if err != nil {
247-
return nil, apperrors.Wrap(err, "failed to list tokenization keys")
248-
}
249-
defer func() {
250-
_ = rows.Close()
251-
}()
252-
253-
var keys []*tokenizationDomain.TokenizationKey
254-
for rows.Next() {
255-
var key tokenizationDomain.TokenizationKey
256-
var id, dekID []byte
257-
var formatType string
258-
259-
err := rows.Scan(
260-
&id,
261-
&key.Name,
262-
&key.Version,
263-
&formatType,
264-
&key.IsDeterministic,
265-
&key.Salt,
266-
&dekID,
267-
&key.CreatedAt,
268-
&key.DeletedAt,
269-
)
270-
if err != nil {
271-
return nil, apperrors.Wrap(err, "failed to scan tokenization key")
272-
}
273-
274-
if err := key.ID.UnmarshalBinary(id); err != nil {
275-
return nil, apperrors.Wrap(err, "failed to unmarshal tokenization key id")
276-
}
277-
278-
if err := key.DekID.UnmarshalBinary(dekID); err != nil {
279-
return nil, apperrors.Wrap(err, "failed to unmarshal dek id")
280-
}
281-
282-
key.FormatType = tokenizationDomain.FormatType(formatType)
283-
keys = append(keys, &key)
284-
}
285-
286-
if err := rows.Err(); err != nil {
287-
return nil, apperrors.Wrap(err, "error iterating tokenization keys")
288-
}
289-
290-
if keys == nil {
291-
keys = make([]*tokenizationDomain.TokenizationKey, 0)
292-
}
293-
294-
return keys, nil
295-
}
296-
297224
// ListCursor retrieves tokenization keys ordered by name ascending using cursor-based pagination.
298225
// Returns the latest version for each key.
299226
func (m *MySQLTokenizationKeyRepository) ListCursor(

internal/tokenization/repository/mysql/mysql_repository_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,96 @@ func TestMySQLTokenizationKeyRepository_GetByName(t *testing.T) {
176176
assert.Equal(t, uint(2), retrieved.Version)
177177
}
178178

179+
func TestMySQLTokenizationKeyRepository_Get(t *testing.T) {
180+
db := testutil.SetupMySQLDB(t)
181+
defer testutil.TeardownDB(t, db)
182+
defer testutil.CleanupMySQLDB(t, db)
183+
184+
repo := NewMySQLTokenizationKeyRepository(db)
185+
ctx := context.Background()
186+
187+
// Create DEK dependency
188+
_, dekID := createKekAndDekMySQL(t, db)
189+
190+
key := &tokenizationDomain.TokenizationKey{
191+
ID: uuid.Must(uuid.NewV7()),
192+
Name: "get-test",
193+
Version: 1,
194+
FormatType: tokenizationDomain.FormatUUID,
195+
IsDeterministic: false,
196+
Salt: []byte("test-salt-32-bytes-long-12345678"),
197+
DekID: dekID,
198+
CreatedAt: time.Now().UTC(),
199+
DeletedAt: nil,
200+
}
201+
202+
err := repo.Create(ctx, key)
203+
require.NoError(t, err)
204+
205+
// Test Get
206+
retrieved, err := repo.Get(ctx, key.ID)
207+
require.NoError(t, err)
208+
assert.Equal(t, key.ID, retrieved.ID)
209+
assert.Equal(t, key.Name, retrieved.Name)
210+
211+
// Test Get NotFound
212+
_, err = repo.Get(ctx, uuid.Must(uuid.NewV7()))
213+
assert.Error(t, err)
214+
assert.ErrorIs(t, err, tokenizationDomain.ErrTokenizationKeyNotFound)
215+
}
216+
217+
func TestMySQLTokenizationKeyRepository_GetByNameAndVersion(t *testing.T) {
218+
db := testutil.SetupMySQLDB(t)
219+
defer testutil.TeardownDB(t, db)
220+
defer testutil.CleanupMySQLDB(t, db)
221+
222+
repo := NewMySQLTokenizationKeyRepository(db)
223+
ctx := context.Background()
224+
225+
// Create DEK dependency
226+
_, dekID := createKekAndDekMySQL(t, db)
227+
228+
key1 := &tokenizationDomain.TokenizationKey{
229+
ID: uuid.Must(uuid.NewV7()),
230+
Name: "version-test",
231+
Version: 1,
232+
FormatType: tokenizationDomain.FormatUUID,
233+
IsDeterministic: false,
234+
Salt: []byte("test-salt-32-bytes-long-12345678"),
235+
DekID: dekID,
236+
CreatedAt: time.Now().UTC(),
237+
DeletedAt: nil,
238+
}
239+
require.NoError(t, repo.Create(ctx, key1))
240+
241+
key2 := &tokenizationDomain.TokenizationKey{
242+
ID: uuid.Must(uuid.NewV7()),
243+
Name: "version-test",
244+
Version: 2,
245+
FormatType: tokenizationDomain.FormatUUID,
246+
IsDeterministic: false,
247+
Salt: []byte("test-salt-32-bytes-long-12345678"),
248+
DekID: dekID,
249+
CreatedAt: time.Now().UTC(),
250+
DeletedAt: nil,
251+
}
252+
require.NoError(t, repo.Create(ctx, key2))
253+
254+
// Test GetByNameAndVersion
255+
retrieved, err := repo.GetByNameAndVersion(ctx, "version-test", 1)
256+
require.NoError(t, err)
257+
assert.Equal(t, key1.ID, retrieved.ID)
258+
259+
retrieved, err = repo.GetByNameAndVersion(ctx, "version-test", 2)
260+
require.NoError(t, err)
261+
assert.Equal(t, key2.ID, retrieved.ID)
262+
263+
// Test NotFound
264+
_, err = repo.GetByNameAndVersion(ctx, "version-test", 3)
265+
assert.Error(t, err)
266+
assert.ErrorIs(t, err, tokenizationDomain.ErrTokenizationKeyNotFound)
267+
}
268+
179269
func TestMySQLTokenizationKeyRepository_Delete(t *testing.T) {
180270
db := testutil.SetupMySQLDB(t)
181271
defer testutil.TeardownDB(t, db)

internal/tokenization/repository/postgresql/postgresql_repository.go

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -176,70 +176,6 @@ func (p *PostgreSQLTokenizationKeyRepository) GetByNameAndVersion(
176176
return &key, nil
177177
}
178178

179-
// List retrieves tokenization keys ordered by name ascending with pagination.
180-
// Returns the latest version for each key.
181-
func (p *PostgreSQLTokenizationKeyRepository) List(
182-
ctx context.Context,
183-
offset, limit int,
184-
) ([]*tokenizationDomain.TokenizationKey, error) {
185-
querier := database.GetTx(ctx, p.db)
186-
187-
query := `
188-
SELECT tk.id, tk.name, tk.version, tk.format_type, tk.is_deterministic, tk.salt, tk.dek_id, tk.created_at, tk.deleted_at
189-
FROM tokenization_keys tk
190-
INNER JOIN (
191-
SELECT name, MAX(version) as max_version
192-
FROM tokenization_keys
193-
WHERE deleted_at IS NULL
194-
GROUP BY name
195-
ORDER BY name ASC
196-
LIMIT $1 OFFSET $2
197-
) latest ON tk.name = latest.name AND tk.version = latest.max_version
198-
ORDER BY tk.name ASC`
199-
200-
rows, err := querier.QueryContext(ctx, query, limit, offset)
201-
if err != nil {
202-
return nil, apperrors.Wrap(err, "failed to list tokenization keys")
203-
}
204-
defer func() {
205-
_ = rows.Close()
206-
}()
207-
208-
var keys []*tokenizationDomain.TokenizationKey
209-
for rows.Next() {
210-
var key tokenizationDomain.TokenizationKey
211-
var formatType string
212-
213-
err := rows.Scan(
214-
&key.ID,
215-
&key.Name,
216-
&key.Version,
217-
&formatType,
218-
&key.IsDeterministic,
219-
&key.Salt,
220-
&key.DekID,
221-
&key.CreatedAt,
222-
&key.DeletedAt,
223-
)
224-
if err != nil {
225-
return nil, apperrors.Wrap(err, "failed to scan tokenization key")
226-
}
227-
228-
key.FormatType = tokenizationDomain.FormatType(formatType)
229-
keys = append(keys, &key)
230-
}
231-
232-
if err := rows.Err(); err != nil {
233-
return nil, apperrors.Wrap(err, "error iterating tokenization keys")
234-
}
235-
236-
if keys == nil {
237-
keys = make([]*tokenizationDomain.TokenizationKey, 0)
238-
}
239-
240-
return keys, nil
241-
}
242-
243179
// ListCursor retrieves tokenization keys ordered by name ascending using cursor-based pagination.
244180
// Returns the latest version for each key.
245181
func (p *PostgreSQLTokenizationKeyRepository) ListCursor(

internal/tokenization/repository/postgresql/postgresql_repository_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,96 @@ func TestPostgreSQLTokenizationKeyRepository_GetByName(t *testing.T) {
176176
assert.Equal(t, uint(2), retrieved.Version)
177177
}
178178

179+
func TestPostgreSQLTokenizationKeyRepository_Get(t *testing.T) {
180+
db := testutil.SetupPostgresDB(t)
181+
defer testutil.TeardownDB(t, db)
182+
defer testutil.CleanupPostgresDB(t, db)
183+
184+
repo := NewPostgreSQLTokenizationKeyRepository(db)
185+
ctx := context.Background()
186+
187+
// Create DEK dependency
188+
_, dekID := createKekAndDek(t, db)
189+
190+
key := &tokenizationDomain.TokenizationKey{
191+
ID: uuid.Must(uuid.NewV7()),
192+
Name: "get-test",
193+
Version: 1,
194+
FormatType: tokenizationDomain.FormatUUID,
195+
IsDeterministic: false,
196+
Salt: []byte("test-salt-32-bytes-long-12345678"),
197+
DekID: dekID,
198+
CreatedAt: time.Now().UTC(),
199+
DeletedAt: nil,
200+
}
201+
202+
err := repo.Create(ctx, key)
203+
require.NoError(t, err)
204+
205+
// Test Get
206+
retrieved, err := repo.Get(ctx, key.ID)
207+
require.NoError(t, err)
208+
assert.Equal(t, key.ID, retrieved.ID)
209+
assert.Equal(t, key.Name, retrieved.Name)
210+
211+
// Test Get NotFound
212+
_, err = repo.Get(ctx, uuid.Must(uuid.NewV7()))
213+
assert.Error(t, err)
214+
assert.ErrorIs(t, err, tokenizationDomain.ErrTokenizationKeyNotFound)
215+
}
216+
217+
func TestPostgreSQLTokenizationKeyRepository_GetByNameAndVersion(t *testing.T) {
218+
db := testutil.SetupPostgresDB(t)
219+
defer testutil.TeardownDB(t, db)
220+
defer testutil.CleanupPostgresDB(t, db)
221+
222+
repo := NewPostgreSQLTokenizationKeyRepository(db)
223+
ctx := context.Background()
224+
225+
// Create DEK dependency
226+
_, dekID := createKekAndDek(t, db)
227+
228+
key1 := &tokenizationDomain.TokenizationKey{
229+
ID: uuid.Must(uuid.NewV7()),
230+
Name: "version-test",
231+
Version: 1,
232+
FormatType: tokenizationDomain.FormatUUID,
233+
IsDeterministic: false,
234+
Salt: []byte("test-salt-32-bytes-long-12345678"),
235+
DekID: dekID,
236+
CreatedAt: time.Now().UTC(),
237+
DeletedAt: nil,
238+
}
239+
require.NoError(t, repo.Create(ctx, key1))
240+
241+
key2 := &tokenizationDomain.TokenizationKey{
242+
ID: uuid.Must(uuid.NewV7()),
243+
Name: "version-test",
244+
Version: 2,
245+
FormatType: tokenizationDomain.FormatUUID,
246+
IsDeterministic: false,
247+
Salt: []byte("test-salt-32-bytes-long-12345678"),
248+
DekID: dekID,
249+
CreatedAt: time.Now().UTC(),
250+
DeletedAt: nil,
251+
}
252+
require.NoError(t, repo.Create(ctx, key2))
253+
254+
// Test GetByNameAndVersion
255+
retrieved, err := repo.GetByNameAndVersion(ctx, "version-test", 1)
256+
require.NoError(t, err)
257+
assert.Equal(t, key1.ID, retrieved.ID)
258+
259+
retrieved, err = repo.GetByNameAndVersion(ctx, "version-test", 2)
260+
require.NoError(t, err)
261+
assert.Equal(t, key2.ID, retrieved.ID)
262+
263+
// Test NotFound
264+
_, err = repo.GetByNameAndVersion(ctx, "version-test", 3)
265+
assert.Error(t, err)
266+
assert.ErrorIs(t, err, tokenizationDomain.ErrTokenizationKeyNotFound)
267+
}
268+
179269
func TestPostgreSQLTokenizationKeyRepository_Delete(t *testing.T) {
180270
db := testutil.SetupPostgresDB(t)
181271
defer testutil.TeardownDB(t, db)

0 commit comments

Comments
 (0)