Skip to content
Open
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
36 changes: 36 additions & 0 deletions server/cmd/api/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,42 @@ func (s *ApiService) StopRecording(ctx context.Context, req oapi.StopRecordingRe
return oapi.StopRecording200Response{}, nil
}

func (s *ApiService) MarkRecording(ctx context.Context, req oapi.MarkRecordingRequestObject) (oapi.MarkRecordingResponseObject, error) {
log := logger.FromContext(ctx)

if req.Body == nil {
return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body is required"}}, nil
}

recorderID := s.defaultRecorderID
if req.Body.Id != nil && *req.Body.Id != "" {
recorderID = *req.Body.Id
}

rec, exists := s.recordManager.GetRecorder(recorderID)
if !exists {
log.Error("attempted to mark non-existent recording", "recorder_id", recorderID)
return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil
}

name, offsetMs, err := rec.Mark(req.Body.Name)
if err != nil {
switch {
case errors.Is(err, recorder.ErrNotRecording):
log.Error("attempted to mark recording that is not in progress", "recorder_id", recorderID)
return oapi.MarkRecording409JSONResponse{ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "no active recording to mark"}}, nil
case errors.Is(err, recorder.ErrInvalidMarkerName):
log.Warn("invalid marker name", "recorder_id", recorderID)
return oapi.MarkRecording400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "marker name must be non-empty and at most 200 characters"}}, nil
default:
log.Error("failed to mark recording", "err", err, "recorder_id", recorderID)
return oapi.MarkRecording500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to mark recording"}}, nil
}
}

return oapi.MarkRecording201JSONResponse{Name: name, OffsetMs: offsetMs}, nil
}

const (
minRecordingSizeInBytes = 100
)
Expand Down
78 changes: 78 additions & 0 deletions server/cmd/api/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,71 @@ func TestApiService_StopRecording(t *testing.T) {
})
}

func TestApiService_MarkRecording(t *testing.T) {
ctx := context.Background()
name := "checkpoint"

t.Run("no recorder maps to 409", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := newSvc(t, mgr)
require.NoError(t, err)

resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}})
require.NoError(t, err)
require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp)
})

t.Run("missing body maps to 400", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
svc, err := newSvc(t, mgr)
require.NoError(t, err)

resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{})
require.NoError(t, err)
require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp)
})

t.Run("not recording maps to 409", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
rec := &mockRecorder{id: "default", markErr: recorder.ErrNotRecording}
require.NoError(t, mgr.RegisterRecorder(ctx, rec))

svc, err := newSvc(t, mgr)
require.NoError(t, err)
resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}})
require.NoError(t, err)
require.IsType(t, oapi.MarkRecording409JSONResponse{}, resp)
require.True(t, rec.markCalled)
})

t.Run("invalid name maps to 400", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
rec := &mockRecorder{id: "default", markErr: recorder.ErrInvalidMarkerName}
require.NoError(t, mgr.RegisterRecorder(ctx, rec))

svc, err := newSvc(t, mgr)
require.NoError(t, err)
resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: " "}})
require.NoError(t, err)
require.IsType(t, oapi.MarkRecording400JSONResponse{}, resp)
})

t.Run("success returns 201 with offset", func(t *testing.T) {
mgr := recorder.NewFFmpegManager()
rec := &mockRecorder{id: "default", isRecordingFlag: true, markOffsetMs: 1234}
require.NoError(t, mgr.RegisterRecorder(ctx, rec))

svc, err := newSvc(t, mgr)
require.NoError(t, err)
resp, err := svc.MarkRecording(ctx, oapi.MarkRecordingRequestObject{Body: &oapi.MarkRecordingJSONRequestBody{Name: name}})
require.NoError(t, err)
r, ok := resp.(oapi.MarkRecording201JSONResponse)
require.True(t, ok, "expected 201 response, got %T", resp)
assert.Equal(t, name, r.Name)
assert.Equal(t, int64(1234), r.OffsetMs)
})
}

func TestApiService_DownloadRecording(t *testing.T) {
ctx := context.Background()

Expand Down Expand Up @@ -226,6 +291,11 @@ type mockRecorder struct {
recordingErr error
recordingData []byte
deleted bool

markCalled bool
markName string
markOffsetMs int64
markErr error
}

func (m *mockRecorder) ID() string { return m.id }
Expand Down Expand Up @@ -257,6 +327,14 @@ func (m *mockRecorder) ForceStop(ctx context.Context) error {
return nil
}

func (m *mockRecorder) Mark(name string) (string, int64, error) {
m.markCalled = true
if m.markErr != nil {
return "", 0, m.markErr
}
return name, m.markOffsetMs, nil
}

func (m *mockRecorder) IsRecording(ctx context.Context) bool { return m.isRecordingFlag }

func (m *mockRecorder) Recording(ctx context.Context) (io.ReadCloser, *recorder.RecordingMetadata, error) {
Expand Down
Loading
Loading