Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display MySQL user defined error in API Key UI #4590

Merged
Merged
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
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 gRPCStoreError(err error, msg string) error {
switch err {
case nil:
if err == nil {
return nil
case datastore.ErrNotFound, filestore.ErrNotFound, stagelogstore.ErrNotFound:
}

Check warning on line 171 in pkg/app/server/grpcapi/grpcapi.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/server/grpcapi/grpcapi.go#L171

Added line #L171 was not covered by tests
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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to change from switch case to multiple if 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we generate an error with a message from MySQL, handling errors with a switch cannot work properly. That's why I had to change it to multi-if blocks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which means the switch block can not check the type of datastore.ErrUserDefined error 🤔 It's a bit weird to me. Btw, how about adding test for this function. Looks like this function is testable without mocking anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an error message from MySQL is added to datastore.ErrUserDefined, checking with the switch block does not work because the new error is no longer the same as datastore.ErrUserDefined.

I agree with you to add tests for it 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added tests.

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 @@
)

const mysqlErrorCodeDuplicateEntry = 1062
const mysqlErrorCodeUserDefined = 1644

// MySQL client wrapper
type MySQL struct {
Expand Down Expand Up @@ -164,8 +165,13 @@
}

_, 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)
}

Check warning on line 174 in pkg/datastore/mysql/mysql.go

View check run for this annotation

Codecov / codecov/patch

pkg/datastore/mysql/mysql.go#L168-L174

Added lines #L168 - L174 were not covered by tests
}
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixes an existing bug that success toast is displayed even if a request fails.

.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 };
Loading