This commit is contained in:
Daniel Noulet 2026-09-19 20:56:27 +02:00
parent 6ffc0d2c99
commit 6153017b62
22 changed files with 835 additions and 100 deletions

View file

@ -317,6 +317,14 @@ func (h *Handler) CreateInvitation(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, webmapper.InvitationToWeb(value, h.frontendURL))
}
func (h *Handler) ReviewInvitation(w http.ResponseWriter, r *http.Request) {
value, err := h.invitations.Review(r.Context(), idParam(r), secondaryIDParam(r, "invitationId"))
if handleServiceError(w, r, err) {
return
}
writeJSON(w, http.StatusOK, webmapper.InvitationReviewToWeb(value, h.frontendURL))
}
func (h *Handler) GetInterview(w http.ResponseWriter, r *http.Request) {
value, err := h.invitations.GetInterview(r.Context(), chi.URLParam(r, "token"))
if handleServiceError(w, r, err) {

View file

@ -19,7 +19,7 @@ func JobToService(value webmodel.JobRequest) servicemodel.JobPosting {
func QuestionToService(value webmodel.QuestionRequest) servicemodel.Question {
return servicemodel.Question{
Text: value.Text, Type: value.Type, Options: value.Options,
ReferenceAnswer: value.ReferenceAnswer,
CodeSnippet: value.CodeSnippet, ReferenceAnswer: value.ReferenceAnswer,
}
}
@ -41,7 +41,8 @@ func JobToWeb(value servicemodel.JobPosting) webmodel.JobResponse {
func QuestionToWeb(value servicemodel.Question) webmodel.QuestionResponse {
return webmodel.QuestionResponse{
ID: value.ID, Text: value.Text, Type: value.Type, Options: value.Options,
ReferenceAnswer: value.ReferenceAnswer, Deprecated: value.Deprecated,
CodeSnippet: value.CodeSnippet, ReferenceAnswer: value.ReferenceAnswer,
Deprecated: value.Deprecated,
}
}
@ -76,7 +77,7 @@ func InvitationToWeb(value servicemodel.Invitation, frontendURL string) webmodel
ID: value.ID, JobID: value.JobID, ParticipantID: value.ParticipantID,
ParticipantName: value.ParticipantName, ParticipantEmail: value.ParticipantEmail,
Token: value.Token, Status: value.Status,
ParticipantURL: frontendURL + "/interview/" + value.Token,
ParticipantURL: frontendURL + "/participant/" + value.Token,
AcceptedAt: value.AcceptedAt, CompletedAt: value.CompletedAt, CreatedAt: value.CreatedAt,
}
}
@ -85,7 +86,8 @@ func InterviewToWeb(value servicemodel.Interview, frontendURL string) webmodel.I
questions := make([]webmodel.InterviewQuestionResponse, len(value.Job.Questions))
for i, question := range value.Job.Questions {
questions[i] = webmodel.InterviewQuestionResponse{
ID: question.ID, Text: question.Text, Type: question.Type, Options: question.Options,
ID: question.ID, Text: question.Text, Type: question.Type,
Options: question.Options, CodeSnippet: question.CodeSnippet,
}
}
return webmodel.InterviewResponse{
@ -99,6 +101,14 @@ func InterviewToWeb(value servicemodel.Interview, frontendURL string) webmodel.I
}
}
func InvitationReviewToWeb(value servicemodel.Interview, frontendURL string) webmodel.InvitationReviewResponse {
return webmodel.InvitationReviewResponse{
Invitation: InvitationToWeb(value.Invitation, frontendURL),
Job: JobToWeb(value.Job),
Answers: value.Answers,
}
}
func traitsToService(values []string) []servicemodel.Trait {
result := make([]servicemodel.Trait, len(values))
for i, value := range values {

View file

@ -0,0 +1,21 @@
package mapper
import (
"testing"
servicemodel "github.com/danyel/go-guess/backend/internal/service/model"
)
func TestInterviewOmitsReferenceAnswer(t *testing.T) {
response := InterviewToWeb(servicemodel.Interview{
Job: servicemodel.JobPosting{
Questions: []servicemodel.Question{{
ID: 1, Type: "code_review", Text: "Review this",
CodeSnippet: "return nil", ReferenceAnswer: "Should return the error",
}},
},
}, "http://localhost")
if len(response.Job.Questions) != 1 || response.Job.Questions[0].CodeSnippet != "return nil" {
t.Fatalf("expected participant-safe code question, got %#v", response)
}
}

View file

@ -52,6 +52,7 @@ type QuestionRequest struct {
Text string `json:"text"`
Type string `json:"type"`
Options []string `json:"options"`
CodeSnippet string `json:"codeSnippet"`
ReferenceAnswer string `json:"referenceAnswer"`
}
@ -60,6 +61,7 @@ type QuestionResponse struct {
Text string `json:"text"`
Type string `json:"type"`
Options []string `json:"options"`
CodeSnippet string `json:"codeSnippet"`
ReferenceAnswer string `json:"referenceAnswer"`
Deprecated bool `json:"deprecated"`
}
@ -123,10 +125,11 @@ type InterviewJobResponse struct {
}
type InterviewQuestionResponse struct {
ID uint `json:"id"`
Text string `json:"text"`
Type string `json:"type"`
Options []string `json:"options"`
ID uint `json:"id"`
Text string `json:"text"`
Type string `json:"type"`
Options []string `json:"options"`
CodeSnippet string `json:"codeSnippet"`
}
type InterviewResponse struct {
@ -135,6 +138,12 @@ type InterviewResponse struct {
Answers map[uint]string `json:"answers"`
}
type InvitationReviewResponse struct {
Invitation InvitationResponse `json:"invitation"`
Job JobResponse `json:"job"`
Answers map[uint]string `json:"answers"`
}
type AnswerRequest struct {
Answer string `json:"answer"`
}

View file

@ -38,6 +38,7 @@ func New(h *handler.Handler, tokens security.ITokenManager, frontendURL string)
protected.Get("/api/jobs/{id}/candidates", h.Candidates)
protected.Get("/api/jobs/{id}/invitations", h.ListInvitations)
protected.Post("/api/jobs/{id}/invitations", h.CreateInvitation)
protected.Get("/api/jobs/{id}/invitations/{invitationId}", h.ReviewInvitation)
protected.Get("/api/questions", h.ListQuestions)
protected.Post("/api/questions", h.CreateQuestion)
protected.Put("/api/questions/{id}", h.UpdateQuestion)

View file

@ -64,6 +64,9 @@ func (invitationStub) List(context.Context, uint) ([]model.Invitation, error) {
func (invitationStub) Create(context.Context, uint, uint) (model.Invitation, error) {
return model.Invitation{}, nil
}
func (invitationStub) Review(context.Context, uint, uint) (model.Interview, error) {
return model.Interview{}, nil
}
func (invitationStub) GetInterview(context.Context, string) (model.Interview, error) {
return model.Interview{}, nil
}