Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion internal/server/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,6 @@ func (h *Handler) VaultByName(w http.ResponseWriter, r *http.Request) {
return
}

// TODO: Implement case insensitive name matching (will need modifiying in store)
name := r.PathValue("name")
if name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
Expand Down
15 changes: 15 additions & 0 deletions internal/server/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,21 @@ func TestVaultByNameEndpoint(t *testing.T) {
}
})

t.Run("case insensitive", func(t *testing.T) {
rec := get("NOTES")
if rec.Code != http.StatusOK {
t.Fatalf("expected status code %d, got %d (body: %s)", http.StatusOK, rec.Code, rec.Body.String())
}

var got store.Vault
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("decode response: %v (body: %s)", err, rec.Body.String())
}
if got.Name != "notes" {
t.Errorf("Name = %q, want stored spelling %q", got.Name, "notes")
}
})

t.Run("not found", func(t *testing.T) {
rec := get("does-not-exist")
if rec.Code != http.StatusNotFound {
Expand Down
16 changes: 14 additions & 2 deletions internal/server/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,19 @@ func (s *Store) CreateVault(ctx context.Context, userID, name string) (Vault, er
UpdatedAt: time.Now(),
}

var existing string
err := s.db.QueryRowContext(ctx, `
SELECT id FROM vaults WHERE user_id = $1 AND name = $2 COLLATE NOCASE`,
userID, name,
).Scan(&existing)
if err == nil {
return Vault{}, ErrVaultExists
}
if err != sql.ErrNoRows {
return Vault{}, fmt.Errorf("store: create vault: %w", err)
}

err = s.db.QueryRowContext(ctx, `
INSERT INTO vaults (id, user_id, name, kdf_salt, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_id, name) DO NOTHING
Expand All @@ -115,7 +127,7 @@ func (s *Store) CreateVault(ctx context.Context, userID, name string) (Vault, er
func (s *Store) VaultByName(ctx context.Context, userID, name string) (Vault, error) {
var vault Vault

err := s.db.QueryRowContext(ctx, "SELECT id, name, kdf_salt, created_at, updated_at FROM vaults WHERE user_id = $1 AND name = $2", userID, name).Scan(&vault.ID, &vault.Name, &vault.KDFSalt, &vault.CreatedAt, &vault.UpdatedAt)
err := s.db.QueryRowContext(ctx, "SELECT id, name, kdf_salt, created_at, updated_at FROM vaults WHERE user_id = $1 AND name = $2 COLLATE NOCASE", userID, name).Scan(&vault.ID, &vault.Name, &vault.KDFSalt, &vault.CreatedAt, &vault.UpdatedAt)
if err != nil {
if err == sql.ErrNoRows {
return Vault{}, ErrVaultNotFound
Expand Down Expand Up @@ -148,7 +160,7 @@ func (s *Store) ListVaults(ctx context.Context, userID string) ([]Vault, error)
}

func (s *Store) DeleteVault(ctx context.Context, userID, name string) (bool, error) {
res, err := s.db.ExecContext(ctx, "DELETE FROM vaults WHERE user_id = $1 AND name = $2", userID, name)
res, err := s.db.ExecContext(ctx, "DELETE FROM vaults WHERE user_id = $1 AND name = $2 COLLATE NOCASE", userID, name)
if err != nil {
return false, fmt.Errorf("store: delete vault: %w", err)
}
Expand Down
54 changes: 54 additions & 0 deletions internal/server/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ func TestCreateVaultDuplicateName(t *testing.T) {
if _, err := s.CreateVault(context.Background(), user.ID, "notes"); err == nil {
t.Error("CreateVault() with a name the user already has expected an error, got nil")
}

if _, err := s.CreateVault(context.Background(), user.ID, "NOTES"); !errors.Is(err, ErrVaultExists) {
t.Errorf("CreateVault(NOTES) error = %v, want ErrVaultExists", err)
}
}

func TestCreateVaultSameNameDifferentUsers(t *testing.T) {
Expand Down Expand Up @@ -167,6 +171,35 @@ func TestVaultByNameScopedToUser(t *testing.T) {
}
}

func TestVaultByNameCaseInsensitive(t *testing.T) {
s := newTestStore(t)
user := seedUser(t, s, "a@example.com")

created, err := s.CreateVault(context.Background(), user.ID, "Notes")
if err != nil {
t.Fatalf("seed CreateVault: %v", err)
}

got, err := s.VaultByName(context.Background(), user.ID, "notes")
if err != nil {
t.Fatalf("VaultByName(notes) error: %v", err)
}
if got.ID != created.ID {
t.Errorf("ID = %q, want %q", got.ID, created.ID)
}
if got.Name != "Notes" {
t.Errorf("Name = %q, want stored spelling %q", got.Name, "Notes")
}

got, err = s.VaultByName(context.Background(), user.ID, "NOTES")
if err != nil {
t.Fatalf("VaultByName(NOTES) error: %v", err)
}
if got.ID != created.ID {
t.Errorf("ID = %q, want %q", got.ID, created.ID)
}
}

func TestListVaults(t *testing.T) {
s := newTestStore(t)
userA := seedUser(t, s, "a@example.com")
Expand Down Expand Up @@ -233,6 +266,27 @@ func TestDeleteVault(t *testing.T) {
}
}

func TestDeleteVaultCaseInsensitive(t *testing.T) {
s := newTestStore(t)
user := seedUser(t, s, "a@example.com")

if _, err := s.CreateVault(context.Background(), user.ID, "Notes"); err != nil {
t.Fatalf("seed CreateVault: %v", err)
}

deleted, err := s.DeleteVault(context.Background(), user.ID, "NOTES")
if err != nil {
t.Fatalf("DeleteVault(NOTES) error: %v", err)
}
if !deleted {
t.Errorf("DeleteVault(NOTES) = false, want true")
}

if _, err := s.VaultByName(context.Background(), user.ID, "Notes"); !errors.Is(err, ErrVaultNotFound) {
t.Errorf("VaultByName() after case-insensitive delete = %v, want ErrVaultNotFound", err)
}
}

func TestDeleteVaultScopedToUser(t *testing.T) {
s := newTestStore(t)
userA := seedUser(t, s, "a@example.com")
Expand Down