Changes
4 changed files (+469/-7)
-
-
@@ -179,3 +179,17 @@ },}, } } func UndefineCustomAttributeDefinition(id string) *eventV1.Event { return &eventV1.Event{ Event: &eventV1.Event_WorkspaceEvent{ WorkspaceEvent: &workspaceEvent.Event{ Event: &workspaceEvent.Event_CustomAttributeUndefined{ CustomAttributeUndefined: &workspaceEvent.CustomAttributeUndefined{ Id: proto.String(id), }, }, }, }, } }
-
-
-
@@ -5,12 +5,19 @@ package workspaceimport ( "context" "slices" "connectrpc.com/connect" "google.golang.org/protobuf/proto" eventV1 "pocka.jp/x/yamori/proto/go/backend/events/v1" "pocka.jp/x/yamori/proto/go/backend/workspace/v1/types" errorV1 "pocka.jp/x/yamori/proto/go/error/v1" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" "pocka.jp/x/yamori/backend/core/event" "pocka.jp/x/yamori/backend/core/projection" workspaceEvent "pocka.jp/x/yamori/backend/events/workspace" ) func deleteCustomAttributeDefinitionSystemError(
-
@@ -20,6 +27,32 @@ return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_SystemError{ SystemError: &errorV1.SystemError{ Message: proto.String(message), }, }, }) } func deleteCustomAttributeDefinitionAuthError() *connect.Response[workspaceV2.DeleteCustomAttributeDefinitionResponse] { return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{ Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_AuthenticationError{ AuthenticationError: &errorV1.AuthenticationError{}, }, }) } func deleteCustomAttributeDefinitionPermError() *connect.Response[workspaceV2.DeleteCustomAttributeDefinitionResponse] { return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{ Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_PermissionError{ PermissionError: &errorV1.PermissionError{}, }, }) } func deleteCustomAttributeDefinitionMissingField(path string) *connect.Response[workspaceV2.DeleteCustomAttributeDefinitionResponse] { return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{ Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_MissingFieldError{ MissingFieldError: &errorV1.MissingFieldError{ Path: proto.String(path), }, }, })
-
@@ -37,5 +70,101 @@ "service", "yamori.workspace.v2.WorkspaceService","method", "DeleteCustomAttributeDefinition", ) return deleteCustomAttributeDefinitionSystemError("Not implemented"), nil logger := s.core.Logger.With( "service", "yamori.workspace.v2.WorkspaceService", "method", "DeleteCustomAttributeDefinition", ) header := req.Header() token, err := s.core.LoadTokenFromCookie(&header) if err != nil || token == nil { return deleteCustomAttributeDefinitionAuthError(), nil } tx, err := s.core.DB.Begin() if err != nil { logger.Error("Failed to begin transaction", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } defer tx.Rollback() secret, err := projection.GetLoginJwtSecret(tx) if err != nil { logger.Error("Failed to read login_jwt_secret projection", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } workspace, err := projection.GetWorkspace(tx) if err != nil { logger.Error("Failed to read workspace projection", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } users, err := projection.GetUsers(tx) if err != nil { logger.Error("Failed to read users projection", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } if err := event.UpdateProjections(tx, workspace, secret, users); err != nil { logger.Error("Failed to update projections", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } user, err := token.FindUser(secret, users) if err != nil { logger.Warn("Malformed token found", "error", err) return deleteCustomAttributeDefinitionAuthError(), nil } if !slices.Contains(user.Permissions, types.Permission_PERMISSION_EDIT_WORKSPACE_PROFILE) { return deleteCustomAttributeDefinitionPermError(), nil } if req.Msg.Id == nil { return deleteCustomAttributeDefinitionMissingField("id"), nil } id := req.Msg.Id.GetValue() if id == "" { return deleteCustomAttributeDefinitionMissingField("id.value"), nil } for _, def := range workspace.Projection.CustomAttributes { if def.GetId() == id { err = event.AppendEvents(tx, []*eventV1.Event{ workspaceEvent.UndefineCustomAttributeDefinition(id), }) if err != nil { logger.Error( "Failed to append custom attribute undefine event", "error", err, ) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } if err := event.UpdateProjections(tx, workspace); err != nil { logger.Error("Failed to update workspace projection", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } if err := tx.Commit(); err != nil { logger.Error("Failed to commit transaction", "error", err) return deleteCustomAttributeDefinitionSystemError("Database error"), nil } return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{ Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_Ok{ Ok: projectionCustomAttributeDefinitionToMessage(def), }, }), nil } } return connect.NewResponse(&workspaceV2.DeleteCustomAttributeDefinitionResponse{ Result: &workspaceV2.DeleteCustomAttributeDefinitionResponse_NotFound{ NotFound: &errorV1.NotFound{ TypeName: proto.String("yamori.workspace.v2.CustomAttributeDefinition"), }, }, }), nil }
-
-
-
@@ -47,6 +47,17 @@ Permissions: &permissions,} } func projectionCustomAttributeDefinitionToMessage( p *projection.Workspace_CustomAttribute, ) *workspaceV2.CustomAttributeDefinition { return &workspaceV2.CustomAttributeDefinition{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: p.Id, }, DisplayName: p.DisplayName, } } func projectionWorkspaceToMessage(p *projection.Workspace, u *projection.Users) *workspaceV2.Workspace { users := make([]*workspaceV2.User, len(u.Users))
-
@@ -57,12 +68,7 @@customAttributeDefs := make([]*workspaceV2.CustomAttributeDefinition, len(p.CustomAttributes)) for i, def := range p.CustomAttributes { customAttributeDefs[i] = &workspaceV2.CustomAttributeDefinition{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: def.Id, }, DisplayName: def.DisplayName, } customAttributeDefs[i] = projectionCustomAttributeDefinitionToMessage(def) } return &workspaceV2.Workspace{
-
-
-
@@ -0,0 +1,313 @@// SPDX-FileCopyrightText: 2025 Shota FUJI <pockawoooh@gmail.com> // SPDX-License-Identifier: AGPL-3.0-only //go:build !js && !wasm package v2 import ( "context" "reflect" "testing" "connectrpc.com/connect" "google.golang.org/protobuf/proto" workspaceV2 "pocka.jp/x/yamori/proto/go/workspace/v2" "pocka.jp/x/yamori/proto/go/workspace/v2/v2connect" ) func TestDeleteCustomAttributeDefinitionOK(t *testing.T) { server, jar := setupLogin(t) httpClient := server.Client() httpClient.Jar = jar client := v2connect.NewWorkspaceServiceClient(httpClient, server.URL()) var fooID string // Create Foo { res, err := client.PutCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.PutCustomAttributeDefinitionRequest{ DisplayName: proto.String("Foo"), }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.PutCustomAttributeDefinitionResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } if v.Ok.GetDisplayName() != "Foo" { t.Fatalf("Expected \"Foo\", got \"%s\"", v.Ok.GetDisplayName()) } fooID = v.Ok.Id.GetValue() } // Create Bar { res, err := client.PutCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.PutCustomAttributeDefinitionRequest{ DisplayName: proto.String("Bar"), }), ) if err != nil { t.Fatal(err) } _, ok := res.Msg.Result.(*workspaceV2.PutCustomAttributeDefinitionResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } } // Delete Foo { res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: proto.String(fooID), }, }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } if v.Ok.GetDisplayName() != "Foo" { t.Fatalf("Expected \"Foo\", got \"%s\"", v.Ok.GetDisplayName()) } } // Get { res, err := client.Get( context.Background(), connect.NewRequest(&workspaceV2.GetRequest{}), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.GetResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Errorf("Expected ok, got %s", typeName.Type().Name()) } if len(v.Ok.CustomAttributeDefinition) != 1 { t.Fatalf("Expected 1 custom attribute definition, got %d", len(v.Ok.CustomAttributeDefinition)) } bar := v.Ok.CustomAttributeDefinition[0].GetDisplayName() if bar != "Bar" { t.Fatalf("Expected \"Bar\", got \"%s\"", bar) } } } func TestDeleteCustomAttributeDefinitionUnauthroizedRequest(t *testing.T) { server := setupInitialAdmin(t) client := v2connect.NewWorkspaceServiceClient(server.Client(), server.URL()) res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: proto.String("foo"), }, }), ) if err != nil { t.Fatal(err) } _, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_AuthenticationError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected authentication_error, got %s", typeName.Type().Name()) } } func TestDeleteCustomAttributeDefinitionPermissionError(t *testing.T) { server, jar := setupRestrictedUserLogin(t) httpClient := server.Client() httpClient.Jar = jar client := v2connect.NewWorkspaceServiceClient(httpClient, server.URL()) res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: proto.String("foo"), }, }), ) if err != nil { t.Fatal(err) } _, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_PermissionError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected permission_error, got %s", typeName.Type().Name()) } } func TestDeleteCustomAttributeDefinitionRejectsMissingID(t *testing.T) { server, jar := setupLogin(t) httpClient := server.Client() httpClient.Jar = jar client := v2connect.NewWorkspaceServiceClient(httpClient, server.URL()) // Create Foo { res, err := client.PutCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.PutCustomAttributeDefinitionRequest{ DisplayName: proto.String("Foo"), }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.PutCustomAttributeDefinitionResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } if v.Ok.GetDisplayName() != "Foo" { t.Fatalf("Expected \"Foo\", got \"%s\"", v.Ok.GetDisplayName()) } } // Delete (no field) { res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{}), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_MissingFieldError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected missing_field, got %s", typeName.Type().Name()) } if v.MissingFieldError.GetPath() != "id" { t.Fatalf("Expected \"id\", got \"%s\"", v.MissingFieldError.GetPath()) } } // Delete (whitespace only) { res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: proto.String(""), }, }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_MissingFieldError) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected missing_field, got %s", typeName.Type().Name()) } if v.MissingFieldError.GetPath() != "id.value" { t.Fatalf("Expected \"id.value\", got \"%s\"", v.MissingFieldError.GetPath()) } } } func TestDeleteCustomAttributeDefinitionRejectsNonexistentID(t *testing.T) { server, jar := setupLogin(t) httpClient := server.Client() httpClient.Jar = jar client := v2connect.NewWorkspaceServiceClient(httpClient, server.URL()) // Create Foo { res, err := client.PutCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.PutCustomAttributeDefinitionRequest{ DisplayName: proto.String("Foo"), }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.PutCustomAttributeDefinitionResponse_Ok) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected ok, got %s", typeName.Type().Name()) } if v.Ok.GetDisplayName() != "Foo" { t.Fatalf("Expected \"Foo\", got \"%s\"", v.Ok.GetDisplayName()) } } // Delete Bar (not found) { res, err := client.DeleteCustomAttributeDefinition( context.Background(), connect.NewRequest(&workspaceV2.DeleteCustomAttributeDefinitionRequest{ Id: &workspaceV2.CustomAttributeDefinitionID{ Value: proto.String("bar"), }, }), ) if err != nil { t.Fatal(err) } v, ok := res.Msg.Result.(*workspaceV2.DeleteCustomAttributeDefinitionResponse_NotFound) if !ok { typeName := reflect.Indirect(reflect.ValueOf(res.Msg.Result)) t.Fatalf("Expected not_found, got %s", typeName.Type().Name()) } if v.NotFound.GetTypeName() != "yamori.workspace.v2.CustomAttributeDefinition" { t.Fatalf( "Expected \"yamori.workspace.v2.CustomAttributeDefinition\", got \"%s\"", v.NotFound.GetTypeName(), ) } } }
-