mirror of
https://github.com/luxfi/captable.git
synced 2026-10-30 17:01:02 +00:00
326 lines
7.3 KiB
Go
326 lines
7.3 KiB
Go
package document
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
type memRepo struct {
|
|
mu sync.RWMutex
|
|
documents map[string]*Document
|
|
dataRooms map[string]*DataRoom
|
|
accessGrants map[string]*AccessGrant
|
|
audit []*AuditEntry
|
|
}
|
|
|
|
func newMemRepo() *memRepo {
|
|
return &memRepo{
|
|
documents: make(map[string]*Document),
|
|
dataRooms: make(map[string]*DataRoom),
|
|
accessGrants: make(map[string]*AccessGrant),
|
|
}
|
|
}
|
|
|
|
func (r *memRepo) CreateDocument(_ context.Context, d *Document) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *d
|
|
r.documents[d.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) GetDocument(_ context.Context, id string) (*Document, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
d, ok := r.documents[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("document %s not found", id)
|
|
}
|
|
cp := *d
|
|
return &cp, nil
|
|
}
|
|
|
|
func (r *memRepo) UpdateDocument(_ context.Context, d *Document) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *d
|
|
r.documents[d.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) ListDocuments(_ context.Context, companyID string) ([]*Document, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Document
|
|
for _, d := range r.documents {
|
|
if d.CompanyID == companyID {
|
|
cp := *d
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) ListByType(_ context.Context, companyID, docType string) ([]*Document, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Document
|
|
for _, d := range r.documents {
|
|
if d.CompanyID == companyID && d.Type == docType {
|
|
cp := *d
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) ListBySecurity(_ context.Context, securityID string) ([]*Document, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Document
|
|
for _, d := range r.documents {
|
|
if d.SecurityID == securityID {
|
|
cp := *d
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) ListByStakeholder(_ context.Context, stakeholderID string) ([]*Document, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*Document
|
|
for _, d := range r.documents {
|
|
if d.StakeholderID == stakeholderID {
|
|
cp := *d
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) CreateDataRoom(_ context.Context, dr *DataRoom) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *dr
|
|
r.dataRooms[dr.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) GetDataRoom(_ context.Context, id string) (*DataRoom, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
dr, ok := r.dataRooms[id]
|
|
if !ok {
|
|
return nil, fmt.Errorf("data room %s not found", id)
|
|
}
|
|
cp := *dr
|
|
return &cp, nil
|
|
}
|
|
|
|
func (r *memRepo) ListDataRooms(_ context.Context, companyID string) ([]*DataRoom, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*DataRoom
|
|
for _, dr := range r.dataRooms {
|
|
if dr.CompanyID == companyID {
|
|
cp := *dr
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) CreateAccessGrant(_ context.Context, ag *AccessGrant) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *ag
|
|
r.accessGrants[ag.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) ListAccessGrants(_ context.Context, dataRoomID string) ([]*AccessGrant, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*AccessGrant
|
|
for _, ag := range r.accessGrants {
|
|
if ag.DataRoomID == dataRoomID {
|
|
cp := *ag
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *memRepo) RevokeAccessGrant(_ context.Context, id string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if ag, ok := r.accessGrants[id]; ok {
|
|
ag.Revoked = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) AppendAudit(_ context.Context, ae *AuditEntry) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
cp := *ae
|
|
r.audit = append(r.audit, &cp)
|
|
return nil
|
|
}
|
|
|
|
func (r *memRepo) ListAudit(_ context.Context, documentID string) ([]*AuditEntry, error) {
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
var out []*AuditEntry
|
|
for _, ae := range r.audit {
|
|
if ae.DocumentID == documentID {
|
|
cp := *ae
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func TestUploadValidation(t *testing.T) {
|
|
svc := NewService(newMemRepo())
|
|
ctx := context.Background()
|
|
|
|
tests := []struct {
|
|
name string
|
|
doc Document
|
|
wantErr string
|
|
}{
|
|
{"missing company", Document{ID: "d1", Name: "X", Type: "disclosure", StorageRef: "s3://x"}, "company_id"},
|
|
{"missing name", Document{ID: "d1", CompanyID: "c1", Type: "disclosure", StorageRef: "s3://x"}, "name"},
|
|
{"missing type", Document{ID: "d1", CompanyID: "c1", Name: "X", StorageRef: "s3://x"}, "type"},
|
|
{"missing storage", Document{ID: "d1", CompanyID: "c1", Name: "X", Type: "disclosure"}, "storage_ref"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := svc.Upload(ctx, &tt.doc)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantErr) {
|
|
t.Fatalf("error %q does not contain %q", err.Error(), tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUploadSuccess(t *testing.T) {
|
|
svc := NewService(newMemRepo())
|
|
ctx := context.Background()
|
|
|
|
doc := &Document{
|
|
ID: "d1",
|
|
CompanyID: "c1",
|
|
Name: "Subscription Agreement",
|
|
Type: "subscription_agreement",
|
|
MimeType: "application/pdf",
|
|
StorageRef: "s3://docs/sub-agreement.pdf",
|
|
Size: 102400,
|
|
}
|
|
if err := svc.Upload(ctx, doc); err != nil {
|
|
t.Fatalf("Upload: %v", err)
|
|
}
|
|
if doc.Status != "draft" {
|
|
t.Fatalf("status = %q, want draft", doc.Status)
|
|
}
|
|
if doc.Version != 1 {
|
|
t.Fatalf("version = %d, want 1", doc.Version)
|
|
}
|
|
}
|
|
|
|
func TestGetRecordsAudit(t *testing.T) {
|
|
repo := newMemRepo()
|
|
svc := NewService(repo)
|
|
ctx := context.Background()
|
|
|
|
repo.CreateDocument(ctx, &Document{
|
|
ID: "d1", CompanyID: "c1", Name: "Doc", Type: "disclosure", StorageRef: "s3://x",
|
|
})
|
|
|
|
_, err := svc.Get(ctx, "d1", "sh1")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
|
|
trail, _ := repo.ListAudit(ctx, "d1")
|
|
if len(trail) != 1 {
|
|
t.Fatalf("audit trail len = %d, want 1", len(trail))
|
|
}
|
|
if trail[0].Action != "view" {
|
|
t.Fatalf("action = %q, want view", trail[0].Action)
|
|
}
|
|
if trail[0].StakeholderID != "sh1" {
|
|
t.Fatalf("stakeholder = %q, want sh1", trail[0].StakeholderID)
|
|
}
|
|
}
|
|
|
|
func TestMarkExecuted(t *testing.T) {
|
|
repo := newMemRepo()
|
|
svc := NewService(repo)
|
|
ctx := context.Background()
|
|
|
|
repo.CreateDocument(ctx, &Document{
|
|
ID: "d1", CompanyID: "c1", Name: "Doc", Type: "subscription_agreement",
|
|
StorageRef: "s3://x", Status: "draft",
|
|
})
|
|
|
|
if err := svc.MarkExecuted(ctx, "d1", "Jane Doe"); err != nil {
|
|
t.Fatalf("MarkExecuted: %v", err)
|
|
}
|
|
|
|
doc, _ := repo.GetDocument(ctx, "d1")
|
|
if doc.Status != "executed" {
|
|
t.Fatalf("status = %q, want executed", doc.Status)
|
|
}
|
|
if doc.SignedBy != "Jane Doe" {
|
|
t.Fatalf("signed_by = %q, want Jane Doe", doc.SignedBy)
|
|
}
|
|
if doc.SignedAt == nil {
|
|
t.Fatal("signed_at should be set")
|
|
}
|
|
}
|
|
|
|
func TestCreateDataRoom(t *testing.T) {
|
|
svc := NewService(newMemRepo())
|
|
ctx := context.Background()
|
|
|
|
dr := &DataRoom{
|
|
ID: "dr1",
|
|
CompanyID: "c1",
|
|
Name: "Series A Data Room",
|
|
}
|
|
if err := svc.CreateDataRoom(ctx, dr); err != nil {
|
|
t.Fatalf("CreateDataRoom: %v", err)
|
|
}
|
|
if dr.Status != "open" {
|
|
t.Fatalf("status = %q, want open", dr.Status)
|
|
}
|
|
}
|
|
|
|
func TestGrantAccess(t *testing.T) {
|
|
svc := NewService(newMemRepo())
|
|
ctx := context.Background()
|
|
|
|
ag := &AccessGrant{
|
|
ID: "ag1",
|
|
DataRoomID: "dr1",
|
|
StakeholderID: "sh1",
|
|
}
|
|
if err := svc.GrantAccess(ctx, ag); err != nil {
|
|
t.Fatalf("GrantAccess: %v", err)
|
|
}
|
|
if ag.Permission != "view" {
|
|
t.Fatalf("permission = %q, want view (default)", ag.Permission)
|
|
}
|
|
}
|