Skip to content

Commit

Permalink
Display MySQL user defined error in API Key UI (pipe-cd#4590)
Browse files Browse the repository at this point in the history
* Display MySQL user defined error in UI

Signed-off-by: Kenta Kozuka <kenta.kozuka@gmail.com>

* Fix Unexpected empty arrow function

Signed-off-by: Kenta Kozuka <kenta.kozuka@gmail.com>

* Add tests

Signed-off-by: Kenta Kozuka <kenta.kozuka@gmail.com>

* Run subtests in parallel

Signed-off-by: Kenta Kozuka <kenta.kozuka@gmail.com>

---------

Signed-off-by: Kenta Kozuka <kenta.kozuka@gmail.com>
  • Loading branch information
kentakozuka authored and sZma5a committed Nov 5, 2023
1 parent 246308b commit df22fb0
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 14 deletions.
18 changes: 11 additions & 7 deletions pkg/app/server/grpcapi/grpcapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,22 @@ func getEncriptionKey(se *model.Piped_SecretEncryption) ([]byte, error) {
}

func gRPCStoreError(err error, msg string) error {
switch err {
case nil:
if err == nil {
return nil
case datastore.ErrNotFound, filestore.ErrNotFound, stagelogstore.ErrNotFound:
}
if errors.Is(err, datastore.ErrNotFound) || errors.Is(err, filestore.ErrNotFound) || errors.Is(err, stagelogstore.ErrNotFound) {
return status.Error(codes.NotFound, fmt.Sprintf("Entity was not found to %s", msg))
case datastore.ErrInvalidArgument:
}
if errors.Is(err, datastore.ErrInvalidArgument) {
return status.Error(codes.InvalidArgument, fmt.Sprintf("Invalid argument to %s", msg))
case datastore.ErrAlreadyExists:
}
if errors.Is(err, datastore.ErrAlreadyExists) {
return status.Error(codes.AlreadyExists, fmt.Sprintf("Entity already exists to %s", msg))
default:
return status.Error(codes.Internal, fmt.Sprintf("Failed to %s", msg))
}
if errors.Is(err, datastore.ErrUserDefined) {
return status.Error(codes.FailedPrecondition, err.Error())
}
return status.Error(codes.Internal, fmt.Sprintf("Failed to %s", msg))
}

func makeUnregisteredAppsCacheKey(projectID string) string {
Expand Down
92 changes: 92 additions & 0 deletions pkg/app/server/grpcapi/grpcapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright 2023 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package grpcapi

import (
"errors"
"fmt"
"testing"

"github.com/pipe-cd/pipecd/pkg/datastore"
"github.com/pipe-cd/pipecd/pkg/filestore"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestGRPCStoreError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
inputErr error
inputMsg string
expected error
}{
{
name: "datastore not found error",
inputErr: datastore.ErrNotFound,
inputMsg: "datastore",
expected: status.Error(codes.NotFound, "Entity was not found to datastore"),
},
{
name: "filestore not found error",
inputErr: filestore.ErrNotFound,
inputMsg: "filestore",
expected: status.Error(codes.NotFound, "Entity was not found to filestore"),
},
{
name: "stagelogstore not found error",
inputErr: filestore.ErrNotFound,
inputMsg: "stagelogstore",
expected: status.Error(codes.NotFound, "Entity was not found to stagelogstore"),
},
{
name: "datastore invalid argument error",
inputErr: datastore.ErrInvalidArgument,
inputMsg: "datastore",
expected: status.Error(codes.InvalidArgument, "Invalid argument to datastore"),
},
{
name: "datastore already exists error",
inputErr: datastore.ErrAlreadyExists,
inputMsg: "datastore",
expected: status.Error(codes.AlreadyExists, "Entity already exists to datastore"),
},
{
name: "user defined error",
inputErr: datastore.ErrUserDefined,
expected: status.Error(codes.FailedPrecondition, "user defined error"),
},
{
name: "user defined error with message",
inputErr: fmt.Errorf("%w: %s", datastore.ErrUserDefined, "test"),
expected: status.Error(codes.FailedPrecondition, "user defined error: test"),
},
{
name: "internal error",
inputErr: errors.New("internal error"),
inputMsg: "test",
expected: status.Error(codes.Internal, "Failed to test"),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := gRPCStoreError(tt.inputErr, tt.inputMsg)
assert.Equal(t, tt.expected, err)
})
}
}
1 change: 1 addition & 0 deletions pkg/datastore/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ var (
ErrInternal = errors.New("internal")
ErrUnimplemented = errors.New("unimplemented")
ErrUnsupported = errors.New("unsupported")
ErrUserDefined = errors.New("user defined error")
)

type Commander string
Expand Down
10 changes: 8 additions & 2 deletions pkg/datastore/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
)

const mysqlErrorCodeDuplicateEntry = 1062
const mysqlErrorCodeUserDefined = 1644

// MySQL client wrapper
type MySQL struct {
Expand Down Expand Up @@ -164,8 +165,13 @@ func (m *MySQL) Create(ctx context.Context, col datastore.Collection, id string,
}

_, err = stmt.ExecContext(ctx, makeRowID(id), data)
if mysqlErr, ok := err.(*mysql.MySQLError); ok && mysqlErr.Number == mysqlErrorCodeDuplicateEntry {
return datastore.ErrAlreadyExists
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == mysqlErrorCodeDuplicateEntry {
return datastore.ErrAlreadyExists
}
if mysqlErr.Number == mysqlErrorCodeUserDefined {
return fmt.Errorf("%w: %s", datastore.ErrUserDefined, mysqlErr.Message)
}
}
if err != nil {
m.logger.Error("failed to create entity",
Expand Down
16 changes: 11 additions & 5 deletions web/src/components/settings-page/api-key/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
DISABLE_API_KEY_SUCCESS,
GENERATE_API_KEY_SUCCESS,
} from "~/constants/toast-text";
import { useAppDispatch, useAppSelector } from "~/hooks/redux";
import { unwrapResult, useAppDispatch, useAppSelector } from "~/hooks/redux";
import {
APIKey,
disableAPIKey,
Expand Down Expand Up @@ -96,10 +96,16 @@ export const APIKeyPage: FC = memo(function APIKeyPage() {

const handleSubmit = useCallback(
(values: { name: string; role: APIKey.Role }) => {
dispatch(generateAPIKey(values)).then(() => {
dispatch(fetchAPIKeys({ enabled: true }));
dispatch(addToast({ message: GENERATE_API_KEY_SUCCESS }));
});
dispatch(generateAPIKey(values))
.then(unwrapResult)
.then(() => {
console.log("handleSubmit.then");
dispatch(fetchAPIKeys({ enabled: true }));
dispatch(
addToast({ message: GENERATE_API_KEY_SUCCESS, severity: "success" })
);
})
.catch(() => undefined);
},
[dispatch]
);
Expand Down
2 changes: 2 additions & 0 deletions web/src/hooks/redux.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @see https://redux-toolkit.js.org/tutorials/typescript#define-typed-hooks
import { unwrapResult } from "@reduxjs/toolkit";
import {
shallowEqual,
TypedUseSelectorHook,
Expand All @@ -15,3 +16,4 @@ export const useShallowEqualSelector: TypedUseSelectorHook<AppState> = (
) => {
return useSelector(selector, shallowEqual);
};
export { unwrapResult };

0 comments on commit df22fb0

Please sign in to comment.