feat: add entity-scoped application events
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
3364eacbcc
commit
e787359c54
40 changed files with 2143 additions and 1437 deletions
|
|
@ -15,11 +15,20 @@ type InterviewNoteEvent struct {
|
|||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type InterviewAttendeeEvent struct {
|
||||
UserID uint `json:"userId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Email string `json:"email"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type InterviewEvent struct {
|
||||
Type string `json:"type"`
|
||||
InterviewID uint `json:"interviewId"`
|
||||
Note *InterviewNoteEvent `json:"note,omitempty"`
|
||||
SharedDocument string `json:"sharedDocument,omitempty"`
|
||||
Type string `json:"type"`
|
||||
InterviewID uint `json:"interviewId"`
|
||||
Note *InterviewNoteEvent `json:"note,omitempty"`
|
||||
Attendee *InterviewAttendeeEvent `json:"attendee,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
SharedDocument string `json:"sharedDocument,omitempty"`
|
||||
}
|
||||
|
||||
type IEventBus interface {
|
||||
|
|
|
|||
|
|
@ -297,14 +297,14 @@ func TestCompleteInterviewWorkflow(t *testing.T) {
|
|||
}, http.StatusOK, &coLogin)
|
||||
|
||||
var inbox []webmodel.ScheduledInterviewResponse
|
||||
requestJSON(t, http.MethodGet, "/api/inbox", coLogin.Token, nil, http.StatusOK, &inbox)
|
||||
requestJSON(t, http.MethodGet, "/api/invitations", coLogin.Token, nil, http.StatusOK, &inbox)
|
||||
if len(inbox) != 1 || inbox[0].ID != scheduled.ID {
|
||||
t.Fatalf("scheduled interview missing from co-interviewer inbox: %#v", inbox)
|
||||
}
|
||||
requestJSON(t, http.MethodPatch, fmt.Sprintf("/api/inbox/%d", scheduled.ID),
|
||||
requestJSON(t, http.MethodPatch, fmt.Sprintf("/api/invitations/%d", scheduled.ID),
|
||||
coLogin.Token, map[string]any{"status": "accepted"}, http.StatusOK, &scheduled)
|
||||
var calendar []webmodel.ScheduledInterviewResponse
|
||||
requestJSON(t, http.MethodGet, "/api/calendar", coLogin.Token, nil, http.StatusOK, &calendar)
|
||||
requestJSON(t, http.MethodGet, "/api/schedule", coLogin.Token, nil, http.StatusOK, &calendar)
|
||||
if len(calendar) != 1 || calendar[0].ID != scheduled.ID {
|
||||
t.Fatalf("accepted interview missing from calendar: %#v", calendar)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,16 @@ func (s *ScheduledInterviewService) UpdateStatus(
|
|||
if !valid {
|
||||
return model.ScheduledInterview{}, fmt.Errorf("%w: invalid interview status transition", ErrInvalidState)
|
||||
}
|
||||
return s.store.UpdateScheduledInterviewStatus(ctx, interviewID, status)
|
||||
updated, err := s.store.UpdateScheduledInterviewStatus(ctx, interviewID, status)
|
||||
if err != nil {
|
||||
return model.ScheduledInterview{}, err
|
||||
}
|
||||
if err := s.bus.PublishInterviewEvent(ctx, eventbus.InterviewEvent{
|
||||
Type: "status.updated", InterviewID: interviewID, Status: updated.Status,
|
||||
}); err != nil {
|
||||
return model.ScheduledInterview{}, fmt.Errorf("publish persisted interview status: %w", err)
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *ScheduledInterviewService) UpdateDocument(
|
||||
|
|
@ -243,10 +252,24 @@ func (s *ScheduledInterviewService) UpdateInbox(
|
|||
if status != "accepted" && status != "declined" {
|
||||
return model.ScheduledInterview{}, fmt.Errorf("%w: attendee status must be accepted or declined", ErrValidation)
|
||||
}
|
||||
if _, err := s.store.UpdateAttendeeStatus(ctx, interviewID, userID, status); err != nil {
|
||||
attendee, err := s.store.UpdateAttendeeStatus(ctx, interviewID, userID, status)
|
||||
if err != nil {
|
||||
return model.ScheduledInterview{}, err
|
||||
}
|
||||
return s.Get(ctx, userID, interviewID)
|
||||
updated, err := s.Get(ctx, userID, interviewID)
|
||||
if err != nil {
|
||||
return model.ScheduledInterview{}, err
|
||||
}
|
||||
if err := s.bus.PublishInterviewEvent(ctx, eventbus.InterviewEvent{
|
||||
Type: "attendee.updated", InterviewID: interviewID,
|
||||
Attendee: &eventbus.InterviewAttendeeEvent{
|
||||
UserID: attendee.UserID, DisplayName: attendee.User.DisplayName,
|
||||
Email: attendee.User.Email, Status: attendee.Status,
|
||||
},
|
||||
}); err != nil {
|
||||
return model.ScheduledInterview{}, fmt.Errorf("publish persisted attendee status: %w", err)
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s *ScheduledInterviewService) ListCalendar(
|
||||
|
|
@ -303,7 +326,7 @@ func (s *ScheduledInterviewService) subscribeToInterview(
|
|||
if event.InterviewID != interviewID {
|
||||
continue
|
||||
}
|
||||
if documentsOnly && event.Type != "document.updated" {
|
||||
if documentsOnly && event.Type != "document.updated" && event.Type != "status.updated" {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type scheduledStore struct {
|
|||
createdIDs []uint
|
||||
status string
|
||||
document string
|
||||
attendee model.InterviewAttendee
|
||||
}
|
||||
|
||||
func (f *scheduledStore) GetInvitation(context.Context, uint, uint) (model.Invitation, error) {
|
||||
|
|
@ -68,6 +69,20 @@ func (f *scheduledStore) UpdateScheduledInterviewDocument(
|
|||
f.interview.SharedDocument = document
|
||||
return f.interview, nil
|
||||
}
|
||||
func (f *scheduledStore) UpdateAttendeeStatus(
|
||||
_ context.Context, _, userID uint, status string,
|
||||
) (model.InterviewAttendee, error) {
|
||||
f.attendee = model.InterviewAttendee{
|
||||
UserID: userID, Status: status,
|
||||
User: model.User{ID: userID, DisplayName: "Ada", Email: "ada@example.com"},
|
||||
}
|
||||
for index := range f.interview.Attendees {
|
||||
if f.interview.Attendees[index].UserID == userID {
|
||||
f.interview.Attendees[index] = f.attendee
|
||||
}
|
||||
}
|
||||
return f.attendee, nil
|
||||
}
|
||||
func (f *scheduledStore) CreateInterviewNote(
|
||||
_ context.Context, value model.InterviewNote,
|
||||
) (model.InterviewNote, error) {
|
||||
|
|
@ -414,6 +429,7 @@ func TestUpdateDocumentPublishesEvent(t *testing.T) {
|
|||
ID: 9, Status: "scheduled", Attendees: []model.InterviewAttendee{{UserID: 1}},
|
||||
},
|
||||
}
|
||||
|
||||
bus := eventbus.NewMemoryBus()
|
||||
events, err := bus.SubscribeInterviewEvents(context.Background())
|
||||
if err != nil {
|
||||
|
|
@ -431,6 +447,36 @@ func TestUpdateDocumentPublishesEvent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStatusAndAttendeeUpdatesPublishEvents(t *testing.T) {
|
||||
store := &scheduledStore{
|
||||
fakeStore: &fakeStore{},
|
||||
interview: model.ScheduledInterview{
|
||||
ID: 9, Status: "scheduled",
|
||||
Attendees: []model.InterviewAttendee{{UserID: 1, Status: "pending"}},
|
||||
},
|
||||
}
|
||||
bus := eventbus.NewMemoryBus()
|
||||
events, err := bus.SubscribeInterviewEvents(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
interviews := NewScheduledInterviewService(store, bus)
|
||||
if _, err := interviews.UpdateStatus(context.Background(), 1, 9, "started"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event := <-events; event.Type != "status.updated" || event.Status != "started" {
|
||||
t.Fatalf("unexpected status event: %#v", event)
|
||||
}
|
||||
store.interview.Status = "scheduled"
|
||||
if _, err := interviews.UpdateInbox(context.Background(), 1, 9, "accepted"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event := <-events; event.Type != "attendee.updated" || event.Attendee == nil ||
|
||||
event.Attendee.UserID != 1 || event.Attendee.Status != "accepted" {
|
||||
t.Fatalf("unexpected attendee event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParticipantSubscriptionFiltersPrivateNotes(t *testing.T) {
|
||||
bus := eventbus.NewMemoryBus()
|
||||
interviews := NewScheduledInterviewService(&fakeStore{}, bus)
|
||||
|
|
@ -455,14 +501,27 @@ func TestParticipantSubscriptionFiltersPrivateNotes(t *testing.T) {
|
|||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bus.PublishInterviewEvent(ctx, eventbus.InterviewEvent{
|
||||
Type: "status.updated", InterviewID: 0, Status: "started",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case event := <-events:
|
||||
if event.Type != "document.updated" || event.SharedDocument != "candidate-safe" {
|
||||
t.Fatalf("participant received unexpected event: %#v", event)
|
||||
t.Fatalf("participant received unexpected document event: %#v", event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("participant did not receive document event")
|
||||
}
|
||||
select {
|
||||
case event := <-events:
|
||||
if event.Type != "status.updated" || event.Status != "started" {
|
||||
t.Fatalf("participant received unexpected event: %#v", event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("participant did not receive status event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCreationHashesPasswordAndUsesCoInterviewerRole(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -47,9 +47,9 @@ func New(h *handler.Handler, tokens security.ITokenManager, frontendURL string)
|
|||
protected.Get("/api/scheduled-interviews/{id}/events", h.InterviewEvents)
|
||||
protected.Get("/api/users", h.ListUsers)
|
||||
protected.Post("/api/users", h.CreateUser)
|
||||
protected.Get("/api/inbox", h.ListInbox)
|
||||
protected.Patch("/api/inbox/{id}", h.UpdateInbox)
|
||||
protected.Get("/api/calendar", h.ListCalendar)
|
||||
protected.Get("/api/invitations", h.ListInbox)
|
||||
protected.Patch("/api/invitations/{id}", h.UpdateInbox)
|
||||
protected.Get("/api/schedule", h.ListCalendar)
|
||||
protected.Get("/api/questions", h.ListQuestions)
|
||||
protected.Post("/api/questions", h.CreateQuestion)
|
||||
protected.Put("/api/questions/{id}", h.UpdateQuestion)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue