forked from baron/baron-sso
kratos SSOT 재설계
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/domain"
|
||||
"baron-sso-backend/internal/service"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -120,6 +123,339 @@ func TestAuditWorksmobileDuplicatePhoneCountryCodesReportsAndFixes(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecreatePendingWorksmobileUsersFromSnapshotCreatesOnlyMatchedUsers(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
tenantID := "22222222-2222-2222-2222-222222222222"
|
||||
userID := "33333333-3333-3333-3333-333333333333"
|
||||
client := &fakeWorksmobilePendingRecreateClient{}
|
||||
output := &strings.Builder{}
|
||||
writer := csv.NewWriter(output)
|
||||
|
||||
counts, err := recreatePendingWorksmobileUsersFromSnapshot(
|
||||
context.Background(),
|
||||
[]service.WorksmobileRemoteUser{
|
||||
{Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID, DisplayName: "Matched"},
|
||||
{Email: "missing@samaneng.com", ID: "works-2", ExternalID: "44444444-4444-4444-4444-444444444444", DisplayName: "Missing"},
|
||||
},
|
||||
func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) {
|
||||
if remote.ExternalID != userID {
|
||||
return domain.User{}, false
|
||||
}
|
||||
return domain.User{
|
||||
ID: userID,
|
||||
Email: "matched@samaneng.com",
|
||||
Name: "Matched User",
|
||||
Status: domain.UserStatusActive,
|
||||
TenantID: &tenantID,
|
||||
}, true
|
||||
},
|
||||
map[string]domain.Tenant{
|
||||
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany},
|
||||
tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID},
|
||||
},
|
||||
nil,
|
||||
"hanmac-family2026",
|
||||
0,
|
||||
writer,
|
||||
client,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err)
|
||||
}
|
||||
if counts.OK != 1 || counts.Skipped != 1 || counts.Errors != 0 {
|
||||
t.Fatalf("counts=%+v, want ok=1 skipped=1 errors=0", counts)
|
||||
}
|
||||
if len(client.patchedUsers) != 1 || client.patchedUsers[0].identifier != "matched@samaneng.com" {
|
||||
t.Fatalf("patched users=%v", client.patchedUsers)
|
||||
}
|
||||
if !strings.Contains(client.patchedUsers[0].payload.Email, ".old") {
|
||||
t.Fatalf("tombstone email=%q", client.patchedUsers[0].payload.Email)
|
||||
}
|
||||
if len(client.patchedUsers[0].payload.AliasEmails) != 0 {
|
||||
t.Fatalf("tombstone alias emails were not cleared: %v", client.patchedUsers[0].payload.AliasEmails)
|
||||
}
|
||||
if len(client.patchedUsers[0].payload.Organizations) == 0 || client.patchedUsers[0].payload.Organizations[0].Email != client.patchedUsers[0].payload.Email {
|
||||
t.Fatalf("tombstone organization email was not updated: %+v", client.patchedUsers[0].payload.Organizations)
|
||||
}
|
||||
if len(client.deletedUsers) != 1 || client.deletedUsers[0] != client.patchedUsers[0].payload.Email {
|
||||
t.Fatalf("deleted users=%v", client.deletedUsers)
|
||||
}
|
||||
if len(client.createdUsers) != 1 {
|
||||
t.Fatalf("created users=%d, want 1", len(client.createdUsers))
|
||||
}
|
||||
if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" {
|
||||
t.Fatal("initial password was not applied to recreated user")
|
||||
}
|
||||
if strings.Contains(output.String(), "missing@samaneng.com") && !strings.Contains(output.String(), "baron user not found") {
|
||||
t.Fatalf("missing user skip reason was not written: %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecreatePendingWorksmobileUsersFromSnapshotRollsBackWhenCreateFails(t *testing.T) {
|
||||
t.Setenv("SAMAN_DOMAIN_ID", "1001")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
tenantID := "22222222-2222-2222-2222-222222222222"
|
||||
userID := "33333333-3333-3333-3333-333333333333"
|
||||
client := &fakeWorksmobilePendingRecreateClient{createErr: errors.New("create failed")}
|
||||
output := &strings.Builder{}
|
||||
writer := csv.NewWriter(output)
|
||||
|
||||
counts, err := recreatePendingWorksmobileUsersFromSnapshot(
|
||||
context.Background(),
|
||||
[]service.WorksmobileRemoteUser{{Email: "matched@samaneng.com", ID: "works-1", ExternalID: userID}},
|
||||
func(ctx context.Context, remote service.WorksmobileRemoteUser) (domain.User, bool) {
|
||||
return domain.User{
|
||||
ID: userID,
|
||||
Email: "matched@samaneng.com",
|
||||
Name: "Matched User",
|
||||
Status: domain.UserStatusActive,
|
||||
TenantID: &tenantID,
|
||||
}, true
|
||||
},
|
||||
map[string]domain.Tenant{
|
||||
rootID: {ID: rootID, Slug: "saman", Name: "삼안", Type: domain.TenantTypeCompany},
|
||||
tenantID: {ID: tenantID, Slug: "team", Name: "Team", Type: domain.TenantTypeOrganization, ParentID: &rootID},
|
||||
},
|
||||
nil,
|
||||
"hanmac-family2026",
|
||||
0,
|
||||
writer,
|
||||
client,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("recreatePendingWorksmobileUsersFromSnapshot returned error: %v", err)
|
||||
}
|
||||
if counts.OK != 0 || counts.Errors != 1 {
|
||||
t.Fatalf("counts=%+v, want ok=0 errors=1", counts)
|
||||
}
|
||||
if len(client.patchedUsers) != 2 {
|
||||
t.Fatalf("patched users=%v", client.patchedUsers)
|
||||
}
|
||||
if client.patchedUsers[1].payload.Email != "matched@samaneng.com" {
|
||||
t.Fatalf("rollback email=%q, want matched@samaneng.com", client.patchedUsers[1].payload.Email)
|
||||
}
|
||||
if !strings.Contains(output.String(), "create failed") || !strings.Contains(output.String(), "ok") {
|
||||
t.Fatalf("rollback result was not written: %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportHanmacWorksmobileUsersFromRowsSkipsExistingRemoteLocalPart(t *testing.T) {
|
||||
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
companyID := "22222222-2222-2222-2222-222222222222"
|
||||
tenantID := "33333333-3333-3333-3333-333333333333"
|
||||
client := &fakeWorksmobilePendingRecreateClient{}
|
||||
store := &fakeHanmacWorksmobileUserStore{}
|
||||
output := &strings.Builder{}
|
||||
writer := csv.NewWriter(output)
|
||||
|
||||
counts, err := importHanmacWorksmobileUsersFromRows(
|
||||
context.Background(),
|
||||
[]hanmacWorksmobileImportRow{{
|
||||
Email: "new@hanmaceng.co.kr",
|
||||
Name: "New User",
|
||||
Role: "user",
|
||||
TenantSlug: "infra-structures",
|
||||
EmployeeID: "M25001",
|
||||
SubEmail: "legacy@hanmaceng.co.kr",
|
||||
}},
|
||||
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
map[string]domain.Tenant{
|
||||
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
map[string]domain.Tenant{
|
||||
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
|
||||
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
[]service.WorksmobileRemoteUser{{
|
||||
Email: "owner@hanmaceng.co.kr",
|
||||
AliasEmails: []string{"legacy@hanmaceng.co.kr"},
|
||||
}},
|
||||
nil,
|
||||
store,
|
||||
"hanmac-family2026",
|
||||
0,
|
||||
true,
|
||||
writer,
|
||||
client,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
|
||||
}
|
||||
if counts.OK != 0 || counts.Skipped != 1 || counts.Errors != 0 {
|
||||
t.Fatalf("counts=%+v, want ok=0 skipped=1 errors=0", counts)
|
||||
}
|
||||
if len(store.saved) != 0 {
|
||||
t.Fatalf("saved users=%d, want 0", len(store.saved))
|
||||
}
|
||||
if len(client.createdUsers) != 0 {
|
||||
t.Fatalf("created Worksmobile users=%d, want 0", len(client.createdUsers))
|
||||
}
|
||||
if !strings.Contains(output.String(), "legacy") || !strings.Contains(output.String(), "local-part already exists") {
|
||||
t.Fatalf("result did not include conflict reason: %s", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportHanmacWorksmobileUsersFromRowsSavesBaronUserAndCreatesWorksmobileUser(t *testing.T) {
|
||||
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
companyID := "22222222-2222-2222-2222-222222222222"
|
||||
tenantID := "33333333-3333-3333-3333-333333333333"
|
||||
client := &fakeWorksmobilePendingRecreateClient{}
|
||||
store := &fakeHanmacWorksmobileUserStore{}
|
||||
output := &strings.Builder{}
|
||||
writer := csv.NewWriter(output)
|
||||
|
||||
counts, err := importHanmacWorksmobileUsersFromRows(
|
||||
context.Background(),
|
||||
[]hanmacWorksmobileImportRow{{
|
||||
Email: "new@hanmaceng.co.kr",
|
||||
Name: "New User",
|
||||
Phone: "010-1234-5678",
|
||||
Role: "user",
|
||||
TenantSlug: "infra-structures",
|
||||
Grade: "과장",
|
||||
EmployeeID: "M25001",
|
||||
SubEmail: "new.alias@hanmaceng.co.kr",
|
||||
}},
|
||||
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
map[string]domain.Tenant{
|
||||
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
map[string]domain.Tenant{
|
||||
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
|
||||
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
store,
|
||||
"hanmac-family2026",
|
||||
0,
|
||||
true,
|
||||
writer,
|
||||
client,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
|
||||
}
|
||||
if counts.OK != 1 || counts.Skipped != 0 || counts.Errors != 0 || counts.BaronCreated != 1 {
|
||||
t.Fatalf("counts=%+v, want ok=1 baronCreated=1", counts)
|
||||
}
|
||||
if len(store.saved) != 1 {
|
||||
t.Fatalf("saved users=%d, want 1", len(store.saved))
|
||||
}
|
||||
if store.saved[0].TenantID == nil || *store.saved[0].TenantID != tenantID {
|
||||
t.Fatalf("saved tenant=%v, want %s", store.saved[0].TenantID, tenantID)
|
||||
}
|
||||
if store.saved[0].Metadata["employee_id"] != "M25001" || store.saved[0].Metadata["sub_email"] != "new.alias@hanmaceng.co.kr" {
|
||||
t.Fatalf("metadata=%v", store.saved[0].Metadata)
|
||||
}
|
||||
if len(client.createdUsers) != 1 {
|
||||
t.Fatalf("created Worksmobile users=%d, want 1", len(client.createdUsers))
|
||||
}
|
||||
if client.createdUsers[0].Email != "new@hanmaceng.co.kr" {
|
||||
t.Fatalf("created email=%q", client.createdUsers[0].Email)
|
||||
}
|
||||
if client.createdUsers[0].PasswordConfig.Password != "hanmac-family2026" {
|
||||
t.Fatal("initial password was not applied")
|
||||
}
|
||||
if !strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "new.alias@hanmaceng.co.kr") {
|
||||
t.Fatalf("alias emails=%v", client.createdUsers[0].AliasEmails)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportHanmacWorksmobileUsersFromRowsKeepsExternalSubEmailOutOfWorksmobileAliases(t *testing.T) {
|
||||
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
companyID := "22222222-2222-2222-2222-222222222222"
|
||||
tenantID := "33333333-3333-3333-3333-333333333333"
|
||||
client := &fakeWorksmobilePendingRecreateClient{}
|
||||
store := &fakeHanmacWorksmobileUserStore{}
|
||||
output := &strings.Builder{}
|
||||
writer := csv.NewWriter(output)
|
||||
|
||||
counts, err := importHanmacWorksmobileUsersFromRows(
|
||||
context.Background(),
|
||||
[]hanmacWorksmobileImportRow{{
|
||||
Email: "external-alias@hanmaceng.co.kr",
|
||||
Name: "External Alias",
|
||||
Role: "user",
|
||||
TenantSlug: "infra-structures",
|
||||
EmployeeID: "M25002",
|
||||
SubEmail: "external@gmail.com",
|
||||
}},
|
||||
domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
map[string]domain.Tenant{
|
||||
"infra-structures": {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
map[string]domain.Tenant{
|
||||
rootID: {ID: rootID, Slug: "hanmac-family", Name: "한맥가족"},
|
||||
companyID: {ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID},
|
||||
tenantID: {ID: tenantID, Slug: "infra-structures", Name: "구조부", Type: domain.TenantTypeOrganization, ParentID: &companyID},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
store,
|
||||
"hanmac-family2026",
|
||||
0,
|
||||
true,
|
||||
writer,
|
||||
client,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("importHanmacWorksmobileUsersFromRows returned error: %v", err)
|
||||
}
|
||||
if counts.OK != 1 || counts.Errors != 0 || counts.Skipped != 0 {
|
||||
t.Fatalf("counts=%+v, want ok=1", counts)
|
||||
}
|
||||
if store.saved[0].Metadata["sub_email"] != nil {
|
||||
t.Fatalf("external sub_email should not be stored as Worksmobile alias metadata: %v", store.saved[0].Metadata)
|
||||
}
|
||||
if store.saved[0].Metadata["external_sub_email"] != "external@gmail.com" {
|
||||
t.Fatalf("external_sub_email=%v", store.saved[0].Metadata["external_sub_email"])
|
||||
}
|
||||
if strings.Contains(strings.Join(client.createdUsers[0].AliasEmails, ","), "external@gmail.com") {
|
||||
t.Fatalf("external sub email was sent as alias: %v", client.createdUsers[0].AliasEmails)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAdminctlWorksmobileOrgUnitPayloadClearsDomainRootParent(t *testing.T) {
|
||||
t.Setenv("HANMAC_DOMAIN_ID", "300286336")
|
||||
rootID := "11111111-1111-1111-1111-111111111111"
|
||||
companyID := "22222222-2222-2222-2222-222222222222"
|
||||
orgID := "33333333-3333-3333-3333-333333333333"
|
||||
root := domain.Tenant{ID: rootID, Slug: "hanmac-family", Name: "한맥가족"}
|
||||
company := domain.Tenant{ID: companyID, Slug: "hanmac", Name: "한맥기술", Type: domain.TenantTypeCompany, ParentID: &rootID}
|
||||
org := domain.Tenant{ID: orgID, Slug: "management-support", Name: "경영지원부", Type: domain.TenantTypeOrganization, ParentID: &companyID}
|
||||
|
||||
payload, err := buildAdminctlWorksmobileOrgUnitPayload(org, root, map[string]domain.Tenant{
|
||||
rootID: root,
|
||||
companyID: company,
|
||||
orgID: org,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("buildAdminctlWorksmobileOrgUnitPayload returned error: %v", err)
|
||||
}
|
||||
if payload.DomainID != 300286336 {
|
||||
t.Fatalf("domainID=%d, want 300286336", payload.DomainID)
|
||||
}
|
||||
if payload.Email != "management-support@hanmaceng.co.kr" {
|
||||
t.Fatalf("email=%q, want management-support@hanmaceng.co.kr", payload.Email)
|
||||
}
|
||||
if payload.ParentOrgUnitID != "" {
|
||||
t.Fatalf("parentOrgUnitID=%q, want empty for domain-root child", payload.ParentOrgUnitID)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeWorksmobilePhoneAuditClient struct {
|
||||
users []service.WorksmobileRemoteUser
|
||||
patches []fakeWorksmobilePhonePatch
|
||||
@@ -138,3 +474,100 @@ func (f *fakeWorksmobilePhoneAuditClient) PatchUser(ctx context.Context, identif
|
||||
f.patches = append(f.patches, fakeWorksmobilePhonePatch{identifier: identifier, payload: payload})
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeWorksmobilePendingRecreateClient struct {
|
||||
createdUsers []service.WorksmobileUserPayload
|
||||
deletedUsers []string
|
||||
undeletedUsers []string
|
||||
patchedUsers []fakeWorksmobilePendingRecreatePatch
|
||||
createErr error
|
||||
}
|
||||
|
||||
type fakeWorksmobilePendingRecreatePatch struct {
|
||||
identifier string
|
||||
payload service.WorksmobileUserPatchPayload
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) CreateOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) UpsertOrgUnit(ctx context.Context, payload service.WorksmobileOrgUnitPayload, matchLocalPart string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) DeleteOrgUnit(ctx context.Context, orgUnitID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) CreateUser(ctx context.Context, payload service.WorksmobileUserPayload) error {
|
||||
if f.createErr != nil {
|
||||
return f.createErr
|
||||
}
|
||||
f.createdUsers = append(f.createdUsers, payload)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) UpsertUser(ctx context.Context, payload service.WorksmobileUserPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) AddUserAliasEmail(ctx context.Context, userID string, email string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) ResetUserPassword(ctx context.Context, userID string, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) DeleteUser(ctx context.Context, userID string) error {
|
||||
f.deletedUsers = append(f.deletedUsers, userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) PatchUser(ctx context.Context, identifier string, payload service.WorksmobileUserPatchPayload) error {
|
||||
f.patchedUsers = append(f.patchedUsers, fakeWorksmobilePendingRecreatePatch{identifier: identifier, payload: payload})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) SetUserActive(ctx context.Context, userID string, active bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) ListUsers(ctx context.Context) ([]service.WorksmobileRemoteUser, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) ListGroups(ctx context.Context) ([]service.WorksmobileRemoteGroup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeWorksmobilePendingRecreateClient) UndeleteUser(ctx context.Context, userID string) error {
|
||||
f.undeletedUsers = append(f.undeletedUsers, userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeHanmacWorksmobileUserStore struct {
|
||||
users map[string]domain.User
|
||||
saved []domain.User
|
||||
}
|
||||
|
||||
func (f *fakeHanmacWorksmobileUserStore) FindByEmail(ctx context.Context, email string) (domain.User, bool, error) {
|
||||
if f.users == nil {
|
||||
return domain.User{}, false, nil
|
||||
}
|
||||
user, ok := f.users[strings.ToLower(strings.TrimSpace(email))]
|
||||
return user, ok, nil
|
||||
}
|
||||
|
||||
func (f *fakeHanmacWorksmobileUserStore) Save(ctx context.Context, user *domain.User) (bool, error) {
|
||||
created := true
|
||||
if f.users == nil {
|
||||
f.users = map[string]domain.User{}
|
||||
} else if _, ok := f.users[strings.ToLower(strings.TrimSpace(user.Email))]; ok {
|
||||
created = false
|
||||
}
|
||||
f.users[strings.ToLower(strings.TrimSpace(user.Email))] = *user
|
||||
f.saved = append(f.saved, *user)
|
||||
return created, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user