package service import ( "context" "testing" "time" filemanager "wucher/internal/domain/file_manager" helicopterfile "wucher/internal/domain/helicopter_file" helicopterfilerealtime "wucher/internal/realtime/helicopterfile" "wucher/internal/shared/pkg/uuidv7" ) type helicopterFileServiceRepoStub struct { listPendingFn func(context.Context, int) ([]helicopterfile.HelicopterFile, error) listPendingBySourceFn func(context.Context, []byte, int) ([]helicopterfile.HelicopterFile, error) updateFn func(context.Context, *helicopterfile.HelicopterFile) error } func (r *helicopterFileServiceRepoStub) Create(context.Context, *helicopterfile.HelicopterFile) error { return nil } func (r *helicopterFileServiceRepoStub) Update(ctx context.Context, row *helicopterfile.HelicopterFile) error { if r.updateFn != nil { return r.updateFn(ctx, row) } return nil } func (r *helicopterFileServiceRepoStub) Delete(context.Context, []byte) error { return nil } func (r *helicopterFileServiceRepoStub) GetByID(context.Context, []byte) (*helicopterfile.HelicopterFile, error) { return nil, nil } func (r *helicopterFileServiceRepoStub) List(context.Context, string, string, int, int) ([]helicopterfile.HelicopterFile, int64, error) { return nil, 0, nil } func (r *helicopterFileServiceRepoStub) ListPendingAttachments(ctx context.Context, limit int) ([]helicopterfile.HelicopterFile, error) { if r.listPendingFn != nil { return r.listPendingFn(ctx, limit) } return nil, nil } func (r *helicopterFileServiceRepoStub) ListPendingAttachmentsBySourceFileID(ctx context.Context, sourceFileID []byte, limit int) ([]helicopterfile.HelicopterFile, error) { if r.listPendingBySourceFn != nil { return r.listPendingBySourceFn(ctx, sourceFileID, limit) } return nil, nil } func (r *helicopterFileServiceRepoStub) ListByHelicopter(context.Context, []byte) ([]helicopterfile.HelicopterFile, error) { return nil, nil } func (r *helicopterFileServiceRepoStub) ListByHelicopterAndSection(context.Context, []byte, string) ([]helicopterfile.HelicopterFile, error) { return nil, nil } func (r *helicopterFileServiceRepoStub) HelicopterExists(context.Context, []byte) (bool, error) { return false, nil } func (r *helicopterFileServiceRepoStub) AttachmentExists(context.Context, []byte) (bool, error) { return false, nil } type helicopterFileServiceManagerStub struct { filemanager.Service createAttachmentFn func(context.Context, filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) getAttachmentByIDFn func(context.Context, []byte) (*filemanager.Attachment, error) markFileActiveFn func(context.Context, []byte, []byte) error createAttachmentArgs []filemanager.CreateAttachmentParams markFileActiveArgs []struct { fileID []byte actorID []byte } } func (m *helicopterFileServiceManagerStub) CreateAttachment(ctx context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) { copied := params copied.FileID = append([]byte(nil), params.FileID...) copied.ActorID = append([]byte(nil), params.ActorID...) m.createAttachmentArgs = append(m.createAttachmentArgs, copied) if m.createAttachmentFn != nil { return m.createAttachmentFn(ctx, params) } return nil, nil } func (m *helicopterFileServiceManagerStub) GetAttachmentByID(ctx context.Context, id []byte) (*filemanager.Attachment, error) { if m.getAttachmentByIDFn != nil { return m.getAttachmentByIDFn(ctx, id) } return nil, nil } func (m *helicopterFileServiceManagerStub) MarkFileActive(ctx context.Context, fileID []byte, actorID []byte) error { m.markFileActiveArgs = append(m.markFileActiveArgs, struct { fileID []byte actorID []byte }{fileID: append([]byte(nil), fileID...), actorID: append([]byte(nil), actorID...)}) if m.markFileActiveFn != nil { return m.markFileActiveFn(ctx, fileID, actorID) } return nil } func (m *helicopterFileServiceManagerStub) ReconcileFileLifecycle(context.Context, []byte, []byte) error { return nil } func (m *helicopterFileServiceManagerStub) CleanupOrphanPendingFiles(context.Context, time.Time, time.Duration, int, bool) (filemanager.FileCleanupStats, error) { return filemanager.FileCleanupStats{}, nil } type helicopterFileServiceEventsStub struct { publishedUsers []string event *helicopterfilerealtime.Event } func (m *helicopterFileServiceEventsStub) PublishToUsers(userUUIDs []string, event helicopterfilerealtime.Event) int { m.publishedUsers = append([]string(nil), userUUIDs...) copied := event m.event = &copied return len(userUUIDs) } func TestHelicopterFileServiceFinalizePendingAttachmentsPublishesReadyEvent(t *testing.T) { actorID := uuidv7.MustBytes() helicopterID := uuidv7.MustBytes() helicopterFileID := uuidv7.MustBytes() sourceFileID := uuidv7.MustBytes() attachmentID := uuidv7.MustBytes() sourceFile := &filemanager.File{ ID: sourceFileID, Name: "purchase_orders 1 (15).xlsx", MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Status: filemanager.FileStatusReady, } repo := &helicopterFileServiceRepoStub{ listPendingFn: func(_ context.Context, limit int) ([]helicopterfile.HelicopterFile, error) { if limit != 1 { t.Fatalf("expected limit 1, got %d", limit) } return []helicopterfile.HelicopterFile{ { ID: helicopterFileID, HelicopterID: helicopterID, SourceFileID: sourceFileID, CreatedBy: actorID, UpdatedBy: actorID, SourceFile: sourceFile, Position: 4, IsMandatory: false, Section: helicopterfile.SectionBeforeFirstFlightInspection, UpdatedAt: time.Unix(100, 0).UTC(), CreatedAt: time.Unix(90, 0).UTC(), FileAttachmentID: nil, }, }, nil }, updateFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error { if row == nil { t.Fatal("expected row to be updated") } if len(row.FileAttachmentID) != 16 { t.Fatalf("expected attachment id to be set, got %d bytes", len(row.FileAttachmentID)) } if len(row.SourceFileID) != 0 { t.Fatalf("expected source file id to be cleared, got %d bytes", len(row.SourceFileID)) } return nil }, } fm := &helicopterFileServiceManagerStub{ createAttachmentFn: func(_ context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) { if got, want := uuidStringOrFail(t, params.FileID), uuidStringOrFail(t, sourceFileID); got != want { t.Fatalf("unexpected file id: got %s want %s", got, want) } if got, want := params.RefType, "helicopter"; got != want { t.Fatalf("unexpected ref type: got %q want %q", got, want) } if got, want := params.RefID, uuidStringOrFail(t, helicopterID); got != want { t.Fatalf("unexpected ref id: got %q want %q", got, want) } if params.Category == nil || *params.Category != "helicopter_file" { t.Fatalf("expected helicopter_file category, got %#v", params.Category) } return &filemanager.Attachment{ ID: attachmentID, FileID: sourceFileID, File: sourceFile, }, nil }, getAttachmentByIDFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) { if got, want := uuidStringOrFail(t, id), uuidStringOrFail(t, attachmentID); got != want { t.Fatalf("unexpected attachment lookup id: got %s want %s", got, want) } return &filemanager.Attachment{ ID: attachmentID, FileID: sourceFileID, File: sourceFile, }, nil }, } events := &helicopterFileServiceEventsStub{} svc := NewHelicopterFileService(repo). WithFileManagerService(fm). WithHelicopterFileEvents(events) finalized, err := svc.FinalizePendingAttachments(context.Background(), 1) if err != nil { t.Fatalf("FinalizePendingAttachments returned error: %v", err) } if finalized != 1 { t.Fatalf("expected one finalized attachment, got %d", finalized) } if len(fm.createAttachmentArgs) != 1 { t.Fatalf("expected one attachment create call, got %d", len(fm.createAttachmentArgs)) } if len(fm.markFileActiveArgs) != 1 { t.Fatalf("expected one mark active call, got %d", len(fm.markFileActiveArgs)) } if events.event == nil { t.Fatal("expected helicopter file event to be published") } expectedUser := uuidStringOrFail(t, actorID) if len(events.publishedUsers) != 1 || events.publishedUsers[0] != expectedUser { t.Fatalf("unexpected published users: %#v", events.publishedUsers) } if events.event.EventType != helicopterfilerealtime.DefaultEventType { t.Fatalf("expected default event type, got %q", events.event.EventType) } if events.event.HelicopterID != uuidStringOrFail(t, helicopterID) { t.Fatalf("unexpected helicopter id: %+v", events.event) } if events.event.HelicopterFileID != uuidStringOrFail(t, helicopterFileID) { t.Fatalf("unexpected helicopter file id: %+v", events.event) } if events.event.FileID != uuidStringOrFail(t, attachmentID) { t.Fatalf("expected finalized event to point to attachment id, got %+v", events.event) } if events.event.AttachmentPending { t.Fatalf("expected finalized event to mark attachment pending false, got %+v", events.event) } if events.event.Name != sourceFile.Name || events.event.MimeType != sourceFile.MimeType { t.Fatalf("unexpected file metadata in event: %+v", events.event) } } func TestHelicopterFileServiceFinalizePendingAttachmentsBySourceFileUUIDPublishesReadyEvent(t *testing.T) { actorID := uuidv7.MustBytes() helicopterID := uuidv7.MustBytes() helicopterFileID := uuidv7.MustBytes() sourceFileID := uuidv7.MustBytes() attachmentID := uuidv7.MustBytes() sourceFileUUID := uuidStringOrFail(t, sourceFileID) sourceFile := &filemanager.File{ ID: sourceFileID, Name: "purchase_orders 1 (15).xlsx", MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", Status: filemanager.FileStatusReady, } repo := &helicopterFileServiceRepoStub{ listPendingBySourceFn: func(_ context.Context, source []byte, limit int) ([]helicopterfile.HelicopterFile, error) { if got, want := uuidStringOrFail(t, source), sourceFileUUID; got != want { t.Fatalf("unexpected source file id: got %s want %s", got, want) } if limit != 1 { t.Fatalf("expected limit 1, got %d", limit) } return []helicopterfile.HelicopterFile{ { ID: helicopterFileID, HelicopterID: helicopterID, SourceFileID: sourceFileID, CreatedBy: actorID, UpdatedBy: actorID, SourceFile: sourceFile, Position: 4, IsMandatory: false, Section: helicopterfile.SectionBeforeFirstFlightInspection, UpdatedAt: time.Unix(100, 0).UTC(), CreatedAt: time.Unix(90, 0).UTC(), FileAttachmentID: nil, }, }, nil }, updateFn: func(_ context.Context, row *helicopterfile.HelicopterFile) error { if row == nil { t.Fatal("expected row to be updated") } if len(row.FileAttachmentID) != 16 { t.Fatalf("expected attachment id to be set, got %d bytes", len(row.FileAttachmentID)) } if len(row.SourceFileID) != 0 { t.Fatalf("expected source file id to be cleared, got %d bytes", len(row.SourceFileID)) } return nil }, } fm := &helicopterFileServiceManagerStub{ createAttachmentFn: func(_ context.Context, params filemanager.CreateAttachmentParams) (*filemanager.Attachment, error) { if got, want := uuidStringOrFail(t, params.FileID), sourceFileUUID; got != want { t.Fatalf("unexpected file id: got %s want %s", got, want) } return &filemanager.Attachment{ ID: attachmentID, FileID: sourceFileID, File: sourceFile, }, nil }, getAttachmentByIDFn: func(_ context.Context, id []byte) (*filemanager.Attachment, error) { if got, want := uuidStringOrFail(t, id), uuidStringOrFail(t, attachmentID); got != want { t.Fatalf("unexpected attachment lookup id: got %s want %s", got, want) } return &filemanager.Attachment{ ID: attachmentID, FileID: sourceFileID, File: sourceFile, }, nil }, } events := &helicopterFileServiceEventsStub{} svc := NewHelicopterFileService(repo). WithFileManagerService(fm). WithHelicopterFileEvents(events) finalized, err := svc.FinalizePendingAttachmentsBySourceFileUUID(context.Background(), sourceFileUUID, 1) if err != nil { t.Fatalf("FinalizePendingAttachmentsBySourceFileUUID returned error: %v", err) } if finalized != 1 { t.Fatalf("expected one finalized attachment, got %d", finalized) } if len(fm.createAttachmentArgs) != 1 { t.Fatalf("expected one attachment create call, got %d", len(fm.createAttachmentArgs)) } if len(fm.markFileActiveArgs) != 1 { t.Fatalf("expected one mark active call, got %d", len(fm.markFileActiveArgs)) } if events.event == nil { t.Fatal("expected helicopter file event to be published") } if events.event.FileID != uuidStringOrFail(t, attachmentID) { t.Fatalf("expected finalized event to point to attachment id, got %+v", events.event) } if events.event.AttachmentPending { t.Fatalf("expected finalized event to mark attachment pending false, got %+v", events.event) } } func uuidStringOrFail(t *testing.T, raw []byte) string { t.Helper() out, err := uuidv7.BytesToString(raw) if err != nil { t.Fatalf("convert uuid: %v", err) } return out }