feat: add edit artist endpoint (section 1.4)
- Add GET/PUT /api/artists/{id} for artist settings
- Update sync to create artists table entry (library settings)
- Support partial updates for monitored, path, quality/metadata profiles
- Add e2e tests for get, edit, partial update flows
This commit is contained in:
@@ -323,6 +323,54 @@ func (h *Handlers) DeleteArtist(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) GetArtist(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.DB == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artistID := chi.URLParam(r, "id")
|
||||||
|
if artistID == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artist, err := h.DB.GetArtistByForeignID(r.Context(), artistID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, artist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) EditArtist(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.DB == nil {
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "database not connected")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artistID := chi.URLParam(r, "id")
|
||||||
|
if artistID == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "artist ID required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var update database.ArtistUpdate
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artist, err := h.DB.UpdateArtistByForeignID(r.Context(), artistID, update)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusNotFound, "artist not found: "+artistID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, artist)
|
||||||
|
}
|
||||||
|
|
||||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func NewRouter(h *Handlers) *chi.Mux {
|
|||||||
r.Post("/sync", h.Sync)
|
r.Post("/sync", h.Sync)
|
||||||
|
|
||||||
r.Route("/artists", func(r chi.Router) {
|
r.Route("/artists", func(r chi.Router) {
|
||||||
|
r.Get("/{id}", h.GetArtist)
|
||||||
|
r.Put("/{id}", h.EditArtist)
|
||||||
r.Post("/{id}/refresh", h.RefreshArtist)
|
r.Post("/{id}/refresh", h.RefreshArtist)
|
||||||
r.Delete("/{id}", h.DeleteArtist)
|
r.Delete("/{id}", h.DeleteArtist)
|
||||||
})
|
})
|
||||||
|
|||||||
+141
-13
@@ -251,19 +251,6 @@ func (db *DB) CountAlbums(ctx context.Context) (int64, error) {
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) GetArtistByForeignID(ctx context.Context, foreignArtistID string) (*ArtistMetadataRow, error) {
|
|
||||||
var a ArtistMetadataRow
|
|
||||||
err := db.pool.QueryRow(ctx, `
|
|
||||||
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
|
|
||||||
FROM artist_metadata
|
|
||||||
WHERE foreign_artist_id = $1
|
|
||||||
`, foreignArtistID).Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) CountAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) (int64, error) {
|
func (db *DB) CountAlbumsByArtist(ctx context.Context, artistMetadataID uuid.UUID) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
err := db.pool.QueryRow(ctx, `
|
err := db.pool.QueryRow(ctx, `
|
||||||
@@ -288,3 +275,144 @@ func (db *DB) DeleteArtistByForeignID(ctx context.Context, foreignArtistID strin
|
|||||||
}
|
}
|
||||||
return result.RowsAffected() > 0, nil
|
return result.RowsAffected() > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArtistRow struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
MetadataID uuid.UUID `json:"metadata_id"`
|
||||||
|
ForeignArtistID string `json:"foreign_artist_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
QualityProfileID *uuid.UUID `json:"quality_profile_id"`
|
||||||
|
MetadataProfileID *uuid.UUID `json:"metadata_profile_id"`
|
||||||
|
RootFolderID *uuid.UUID `json:"root_folder_id"`
|
||||||
|
Path *string `json:"path"`
|
||||||
|
Monitored bool `json:"monitored"`
|
||||||
|
MonitorNewItems string `json:"monitor_new_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpsertArtist(ctx context.Context, metadataID uuid.UUID) (uuid.UUID, error) {
|
||||||
|
var existingID uuid.UUID
|
||||||
|
err := db.pool.QueryRow(ctx, `
|
||||||
|
SELECT id FROM artists WHERE metadata_id = $1
|
||||||
|
`, metadataID).Scan(&existingID)
|
||||||
|
if err == nil {
|
||||||
|
return existingID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultID uuid.UUID
|
||||||
|
err = db.pool.QueryRow(ctx, `
|
||||||
|
INSERT INTO artists (metadata_id, monitored, monitor_new_items)
|
||||||
|
VALUES ($1, true, 'all')
|
||||||
|
RETURNING id
|
||||||
|
`, metadataID).Scan(&resultID)
|
||||||
|
return resultID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetArtistByForeignID(ctx context.Context, foreignArtistID string) (*ArtistRow, error) {
|
||||||
|
var a ArtistRow
|
||||||
|
err := db.pool.QueryRow(ctx, `
|
||||||
|
SELECT a.id, a.metadata_id, am.foreign_artist_id, am.name,
|
||||||
|
a.quality_profile_id, a.metadata_profile_id, a.root_folder_id,
|
||||||
|
a.path, a.monitored, a.monitor_new_items
|
||||||
|
FROM artists a
|
||||||
|
JOIN artist_metadata am ON a.metadata_id = am.id
|
||||||
|
WHERE am.foreign_artist_id = $1
|
||||||
|
`, foreignArtistID).Scan(
|
||||||
|
&a.ID, &a.MetadataID, &a.ForeignArtistID, &a.Name,
|
||||||
|
&a.QualityProfileID, &a.MetadataProfileID, &a.RootFolderID,
|
||||||
|
&a.Path, &a.Monitored, &a.MonitorNewItems,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistUpdate struct {
|
||||||
|
QualityProfileID *string `json:"quality_profile_id"`
|
||||||
|
MetadataProfileID *string `json:"metadata_profile_id"`
|
||||||
|
RootFolderID *string `json:"root_folder_id"`
|
||||||
|
Path *string `json:"path"`
|
||||||
|
Monitored *bool `json:"monitored"`
|
||||||
|
MonitorNewItems *string `json:"monitor_new_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) UpdateArtistByForeignID(ctx context.Context, foreignArtistID string, update ArtistUpdate) (*ArtistRow, error) {
|
||||||
|
metadataRow, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.Monitored != nil {
|
||||||
|
_, err = db.pool.Exec(ctx, `
|
||||||
|
UPDATE artists SET monitored = $1 WHERE metadata_id = $2
|
||||||
|
`, *update.Monitored, metadataRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.Path != nil {
|
||||||
|
_, err = db.pool.Exec(ctx, `
|
||||||
|
UPDATE artists SET path = $1 WHERE metadata_id = $2
|
||||||
|
`, *update.Path, metadataRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.QualityProfileID != nil {
|
||||||
|
var qpID *uuid.UUID
|
||||||
|
if *update.QualityProfileID != "" {
|
||||||
|
parsed, err := uuid.Parse(*update.QualityProfileID)
|
||||||
|
if err == nil {
|
||||||
|
qpID = &parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = db.pool.Exec(ctx, `
|
||||||
|
UPDATE artists SET quality_profile_id = $1 WHERE metadata_id = $2
|
||||||
|
`, qpID, metadataRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.RootFolderID != nil {
|
||||||
|
var rfID *uuid.UUID
|
||||||
|
if *update.RootFolderID != "" {
|
||||||
|
parsed, err := uuid.Parse(*update.RootFolderID)
|
||||||
|
if err == nil {
|
||||||
|
rfID = &parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = db.pool.Exec(ctx, `
|
||||||
|
UPDATE artists SET root_folder_id = $1 WHERE metadata_id = $2
|
||||||
|
`, rfID, metadataRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.MonitorNewItems != nil {
|
||||||
|
_, err = db.pool.Exec(ctx, `
|
||||||
|
UPDATE artists SET monitor_new_items = $1 WHERE metadata_id = $2
|
||||||
|
`, *update.MonitorNewItems, metadataRow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.GetArtistByForeignID(ctx, foreignArtistID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) GetArtistMetadataByForeignID(ctx context.Context, foreignArtistID string) (*ArtistMetadataRow, error) {
|
||||||
|
var a ArtistMetadataRow
|
||||||
|
err := db.pool.QueryRow(ctx, `
|
||||||
|
SELECT id, foreign_artist_id, name, sort_name, artist_type, genres, created_at, updated_at
|
||||||
|
FROM artist_metadata
|
||||||
|
WHERE foreign_artist_id = $1
|
||||||
|
`, foreignArtistID).Scan(&a.ID, &a.ForeignArtistID, &a.Name, &a.SortName, &a.ArtistType, &a.Genres, &a.CreatedAt, &a.UpdatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ func Sync(
|
|||||||
idStr := id.String()
|
idStr := id.String()
|
||||||
artistMetadataID = &idStr
|
artistMetadataID = &idStr
|
||||||
log.Info().Str("artist", artist.Name).Str("id", idStr).Msg("stored artist metadata")
|
log.Info().Str("artist", artist.Name).Str("id", idStr).Msg("stored artist metadata")
|
||||||
|
|
||||||
|
if _, err := db.UpsertArtist(ctx, id); err != nil {
|
||||||
|
log.Warn().Err(err).Str("artist", artist.Name).Msg("failed to create artist library entry")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,7 +310,7 @@ func RefreshArtist(
|
|||||||
return nil, &NotFoundError{Message: "database not available"}
|
return nil, &NotFoundError{Message: "database not available"}
|
||||||
}
|
}
|
||||||
|
|
||||||
existingArtist, err := db.GetArtistByForeignID(ctx, foreignArtistID)
|
existingArtist, err := db.GetArtistMetadataByForeignID(ctx, foreignArtistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &NotFoundError{Message: "artist not found: " + foreignArtistID}
|
return nil, &NotFoundError{Message: "artist not found: " + foreignArtistID}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package e2e
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fujin/music-agregator/testing/e2e/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEditArtist_Flow covers section 1.4 of FLOWS.md:
|
||||||
|
// 1. User changes quality profile, root folder, monitoring status
|
||||||
|
// 2. Persist to artists table
|
||||||
|
func TestEditArtist_Flow(t *testing.T) {
|
||||||
|
env := testutil.NewTestEnv(t)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
artistName := "Morcheeba"
|
||||||
|
|
||||||
|
if err := env.CleanupArtistByName(ctx, artistName); err != nil {
|
||||||
|
t.Fatalf("cleanup failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncResp, err := env.POST("/api/sync", map[string]any{
|
||||||
|
"artist": artistName,
|
||||||
|
"store": true,
|
||||||
|
"download": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sync failed: %v", err)
|
||||||
|
}
|
||||||
|
syncResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var syncResult struct {
|
||||||
|
ArtistID string `json:"artist_id"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
}
|
||||||
|
if err := syncResp.DecodeJSON(&syncResult); err != nil {
|
||||||
|
t.Fatalf("failed to decode sync response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
artistID := syncResult.ArtistID
|
||||||
|
|
||||||
|
t.Run("GetArtistSettings", func(t *testing.T) {
|
||||||
|
getResp, err := env.GET("/api/artists/" + artistID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get artist failed: %v", err)
|
||||||
|
}
|
||||||
|
getResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var artist struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Monitored bool `json:"monitored"`
|
||||||
|
}
|
||||||
|
if err := getResp.DecodeJSON(&artist); err != nil {
|
||||||
|
t.Fatalf("failed to decode artist: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if artist.Name != syncResult.ArtistName {
|
||||||
|
t.Errorf("expected name=%q, got %q", syncResult.ArtistName, artist.Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateMonitoredStatus", func(t *testing.T) {
|
||||||
|
editResp, err := env.PUT("/api/artists/"+artistID, map[string]any{
|
||||||
|
"monitored": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("edit request failed: %v", err)
|
||||||
|
}
|
||||||
|
editResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Monitored bool `json:"monitored"`
|
||||||
|
}
|
||||||
|
if err := editResp.DecodeJSON(&result); err != nil {
|
||||||
|
t.Fatalf("failed to decode edit response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Monitored != false {
|
||||||
|
t.Error("expected monitored=false after edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
getResp, _ := env.GET("/api/artists/" + artistID)
|
||||||
|
var artist struct {
|
||||||
|
Monitored bool `json:"monitored"`
|
||||||
|
}
|
||||||
|
getResp.DecodeJSON(&artist)
|
||||||
|
if artist.Monitored != false {
|
||||||
|
t.Error("expected monitored=false to persist")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateQualityProfile", func(t *testing.T) {
|
||||||
|
editResp, err := env.PUT("/api/artists/"+artistID, map[string]any{
|
||||||
|
"quality_profile_id": "test-profile-id",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("edit request failed: %v", err)
|
||||||
|
}
|
||||||
|
editResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
QualityProfileID *string `json:"quality_profile_id"`
|
||||||
|
}
|
||||||
|
if err := editResp.DecodeJSON(&result); err != nil {
|
||||||
|
t.Fatalf("failed to decode edit response: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UpdateRootFolder", func(t *testing.T) {
|
||||||
|
editResp, err := env.PUT("/api/artists/"+artistID, map[string]any{
|
||||||
|
"root_folder_id": "test-folder-id",
|
||||||
|
"path": "/music/morcheeba",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("edit request failed: %v", err)
|
||||||
|
}
|
||||||
|
editResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
RootFolderID *string `json:"root_folder_id"`
|
||||||
|
Path *string `json:"path"`
|
||||||
|
}
|
||||||
|
if err := editResp.DecodeJSON(&result); err != nil {
|
||||||
|
t.Fatalf("failed to decode edit response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Path == nil || *result.Path != "/music/morcheeba" {
|
||||||
|
t.Errorf("expected path=/music/morcheeba, got %v", result.Path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
env.CleanupArtistByName(context.Background(), syncResult.ArtistName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditArtist_NotFound(t *testing.T) {
|
||||||
|
env := testutil.NewTestEnv(t)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
editResp, err := env.PUT("/api/artists/nonexistent-artist-id-99999", map[string]any{
|
||||||
|
"monitored": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("edit request failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
editResp.AssertStatus(t, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEditArtist_PartialUpdate(t *testing.T) {
|
||||||
|
env := testutil.NewTestEnv(t)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
artistName := "Zero 7"
|
||||||
|
|
||||||
|
if err := env.CleanupArtistByName(ctx, artistName); err != nil {
|
||||||
|
t.Fatalf("cleanup failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncResp, err := env.POST("/api/sync", map[string]any{
|
||||||
|
"artist": artistName,
|
||||||
|
"store": true,
|
||||||
|
"download": false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sync failed: %v", err)
|
||||||
|
}
|
||||||
|
syncResp.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
var syncResult struct {
|
||||||
|
ArtistID string `json:"artist_id"`
|
||||||
|
ArtistName string `json:"artist_name"`
|
||||||
|
}
|
||||||
|
syncResp.DecodeJSON(&syncResult)
|
||||||
|
|
||||||
|
firstEdit, err := env.PUT("/api/artists/"+syncResult.ArtistID, map[string]any{
|
||||||
|
"monitored": false,
|
||||||
|
"path": "/music/zero7",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first edit failed: %v", err)
|
||||||
|
}
|
||||||
|
firstEdit.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
secondEdit, err := env.PUT("/api/artists/"+syncResult.ArtistID, map[string]any{
|
||||||
|
"monitored": true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second edit failed: %v", err)
|
||||||
|
}
|
||||||
|
secondEdit.AssertStatus(t, 200)
|
||||||
|
|
||||||
|
getResp, _ := env.GET("/api/artists/" + syncResult.ArtistID)
|
||||||
|
var artist struct {
|
||||||
|
Monitored bool `json:"monitored"`
|
||||||
|
Path *string `json:"path"`
|
||||||
|
}
|
||||||
|
getResp.DecodeJSON(&artist)
|
||||||
|
|
||||||
|
if artist.Monitored != true {
|
||||||
|
t.Error("expected monitored=true after second edit")
|
||||||
|
}
|
||||||
|
|
||||||
|
if artist.Path == nil || *artist.Path != "/music/zero7" {
|
||||||
|
t.Errorf("expected path to be preserved, got %v", artist.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
env.CleanupArtistByName(context.Background(), syncResult.ArtistName)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -188,6 +188,11 @@ func (e *TestEnv) DELETE(path string) (*APIResponse, error) {
|
|||||||
return e.Do(APIRequest{Method: http.MethodDelete, Path: path})
|
return e.Do(APIRequest{Method: http.MethodDelete, Path: path})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PUT is a convenience method for PUT requests.
|
||||||
|
func (e *TestEnv) PUT(path string, body any) (*APIResponse, error) {
|
||||||
|
return e.Do(APIRequest{Method: http.MethodPut, Path: path, Body: body})
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeJSON decodes the response body into the given value.
|
// DecodeJSON decodes the response body into the given value.
|
||||||
func (r *APIResponse) DecodeJSON(v any) error {
|
func (r *APIResponse) DecodeJSON(v any) error {
|
||||||
return json.Unmarshal(r.Body, v)
|
return json.Unmarshal(r.Body, v)
|
||||||
|
|||||||
Reference in New Issue
Block a user