Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 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
8 changes: 8 additions & 0 deletions .dredd/hooks/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var view *db.View
var integration *db.Integration
var integrationextractvalue *db.IntegrationExtractValue
var integrationmatch *db.IntegrationMatcher
var invite *db.ProjectInvite

// Runtime created simple ID values for some items we need to reference in other objects
var repoID int
Expand All @@ -45,6 +46,7 @@ var capabilities = map[string][]string{
"integration": {"project", "template"},
"integrationextractvalue": {"integration"},
"integrationmatcher": {"integration"},
"invite": {"user", "project"},
}

func capabilityWrapper(cap string) func(t *trans.Transaction) {
Expand Down Expand Up @@ -76,6 +78,8 @@ func resolveCapability(caps []string, resolved []string, uid string) {

//Add dep specific stuff
switch v {
case "invite":
invite = addInvite()
case "schedule":
schedule = addSchedule()
case "view":
Expand Down Expand Up @@ -199,6 +203,7 @@ var pathSubPatterns = []func() string{
func() string { return strconv.Itoa(integration.ID) },
func() string { return strconv.Itoa(integrationextractvalue.ID) },
func() string { return strconv.Itoa(integrationmatch.ID) },
func() string { return strconv.Itoa(invite.ID) }, // invite_id, x-example: 14
}

// alterRequestPath with the above slice of functions
Expand Down Expand Up @@ -230,6 +235,9 @@ func alterRequestBody(t *trans.Transaction) {
bodyFieldProcessor("ssh_key_id", userKey.ID, &request)
bodyFieldProcessor("become_key_id", userKey.ID, &request)
}
if invite != nil {
bodyFieldProcessor("invite_id", 4, &request)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Hardcoded Invite ID Causes Inconsistent Processing

The invite_id in the bodyFieldProcessor function is hardcoded to 4. This is inconsistent with how other object IDs (e.g., task.ID, schedule.ID, environmentID, templateID) are handled in the same function, which use dynamic invite.ID values. This inconsistency can lead to incorrect invite_id values being processed in requests.

Fix in Cursor Fix in Web

bodyFieldProcessor("environment_id", environmentID, &request)
bodyFieldProcessor("inventory_id", inventoryID, &request)
bodyFieldProcessor("repository_id", repoID, &request)
Expand Down
32 changes: 31 additions & 1 deletion .dredd/hooks/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package main
import (
"encoding/json"
"fmt"
"github.com/semaphoreui/semaphore/pkg/tz"
"os"

"github.com/semaphoreui/semaphore/pkg/tz"

"github.com/go-gorp/gorp/v3"
"github.com/semaphoreui/semaphore/db"
"github.com/semaphoreui/semaphore/db/bolt"
Expand Down Expand Up @@ -195,6 +196,35 @@ func addView() *db.View {
return &view
}

func addInvite() *db.ProjectInvite {
invite, err := store.CreateProjectInvite(db.ProjectInvite{
ProjectID: userProject.ID,
UserID: &userPathTestUser.ID,
Email: &userPathTestUser.Email,
Role: "owner",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Invite API Validation Fails on Dual User ID and Email

The addInvite() helper function creates a ProjectInvite with both UserID and Email set. However, the API validation in api/projects/invites.go requires exactly one of these fields (not both), causing validation to fail with the error "Either user_id or email must be provided, but not both".

Fix in Cursor Fix in Web

Status: db.ProjectInvitePending,
Token: getUUID(),
InviterUserID: testRunnerUser.ID,
Created: tz.Now(),
ExpiresAt: nil, // No expiration for this test
AcceptedAt: nil,
})

fmt.Println("***************************************")
fmt.Println("***************************************")
fmt.Println("***************************************")
fmt.Println(invite.ID)
fmt.Println("***************************************")
fmt.Println("***************************************")
fmt.Println("***************************************")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unintended Debug Statements in Code

Debug print statements, including multiple lines of asterisks and invite.ID, were accidentally committed in the addInvite() function. These should be removed as they are not intended for production and clutter console output.

Fix in Cursor Fix in Web

if err != nil {
panic(err)
}

return &invite
}

func addSchedule() *db.Schedule {
schedule, err := store.CreateSchedule(db.Schedule{
TemplateID: int(templateID),
Expand Down
6 changes: 6 additions & 0 deletions .dredd/hooks/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func main() {
transaction.Request.Body = "{ \"user_id\": " + strconv.Itoa(userPathTestUser.ID) + ",\"role\": \"owner\"}"
})

h.Before("project > /api/project/{project_id}/invites > Get invitations for project > 200 > application/json", capabilityWrapper("invite"))
h.Before("project > /api/project/{project_id}/invites > Create project invitation > 201 > application/json", capabilityWrapper("invite"))
h.Before("project > /api/project/{project_id}/invites/{invite_id} > Get specific project invitation > 200 > application/json", capabilityWrapper("invite"))
h.Before("project > /api/project/{project_id}/invites/{invite_id} > Update project invitation status > 204 > application/json", capabilityWrapper("invite"))
h.Before("project > /api/project/{project_id}/invites/{invite_id} > Delete project invitation > 204 > application/json", capabilityWrapper("invite"))

h.Before("project > /api/project/{project_id}/integrations > get all integrations > 200 > application/json", capabilityWrapper("integration"))
h.Before("project > /api/project/{project_id}/integrations/{integration_id} > Get Integration > 200 > application/json", capabilityWrapper("integration"))
h.Before("project > /api/project/{project_id}/integrations/{integration_id} > Update Integration > 204 > application/json", capabilityWrapper("integration"))
Expand Down
202 changes: 202 additions & 0 deletions api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,87 @@ definitions:
type: string
enum: [owner, manager, task_runner, guest]

ProjectInvite:
type: object
properties:
id:
type: integer
minimum: 1
project_id:
type: integer
minimum: 1
user_id:
type: integer
minimum: 1
description: User ID for user-based invites (optional)
email:
type: string
format: email
description: Email address for email-based invites (optional)
role:
type: string
enum: [owner, manager, task_runner, guest]
example: manager
status:
type: string
enum: [pending, accepted, declined, expired]
example: pending
inviter_user_id:
type: integer
minimum: 1
description: ID of the user who created the invite
created:
type: string
format: date-time
description: When the invite was created
expires_at:
type: string
format: date-time
description: When the invite expires (optional)
accepted_at:
type: string
format: date-time
description: When the invite was accepted (optional)
inviter_user:
$ref: "#/definitions/User"
description: Details of the user who created the invite
user:
$ref: "#/definitions/User"
description: Details of the invited user (for user-based invites)

ProjectInviteRequest:
type: object
properties:
# user_id:
# type: integer
# minimum: 1
# description: User ID to invite (use either user_id or email, not both)
email:
type: string
format: email
description: Email address to invite (use either user_id or email, not both)
x-example: [email protected]
role:
type: string
enum: [owner, manager, task_runner, guest]
example: manager
expires_at:
type: string
format: date-time
description: When the invite should expire (optional, defaults to 7 days)
required:
- role

AcceptInviteRequest:
type: object
properties:
token:
type: string
description: The invitation token
x-example: "a1b2c3d4e5f6..."
required:
- token

ProjectBackup:
type: object
example: {"meta":{"name":"homelab","alert":true,"alert_chat":"Test","max_parallel_tasks":0,"type":null},"templates":[{"inventory":"Build","repository":"Demo","environment":"Empty","name":"Build","playbook":"build.yml","arguments":"[]","allow_override_args_in_task":false,"description":"Build Job","vault_key":null,"type":"build","start_version":"1.0.0","build_template":null,"view":"Build","autorun":false,"survey_vars":[],"suppress_success_alerts":false,"cron":"* * * * *"}],"repositories":[{"name":"Demo","git_url":"https://github.com/semaphoreui/semaphore-demo.git","git_branch":"main","ssh_key":"None"}],"keys":[{"name":"None","type":"none"},{"name":"Vault Password","type":"login_password"}],"views":[{"title":"Build","position":0}],"inventories":[{"name":"Build","inventory":"","ssh_key":"None","become_key":"None","type":"static"},{"name":"Dev","inventory":"","ssh_key":"None","become_key":"None","type":"file"},{"name":"Prod","inventory":"","ssh_key":"None","become_key":"None","type":"file"}],"environments":[{"name":"Empty","password":null,"json":"{}","env":null}]}
Expand Down Expand Up @@ -1130,6 +1211,13 @@ parameters:
type: integer
required: true
x-example: 13
invite_id:
name: invite_id
description: Invite ID
in: path
type: integer
required: true
x-example: 14

paths:
/debug/gc:
Expand Down Expand Up @@ -1632,6 +1720,120 @@ paths:
204:
description: User updated

# Invite management
/project/{project_id}/invites:
parameters:
- $ref: "#/parameters/project_id"
get:
tags:
- project
summary: Get invitations for project
parameters:
- name: sort
in: query
required: false
type: string
enum: [created, status, role]
description: sorting field
x-example: created
- name: order
in: query
required: false
type: string
enum: [asc, desc]
description: ordering manner
x-example: desc
responses:
200:
description: Project invitations
schema:
type: array
items:
$ref: "#/definitions/ProjectInvite"
post:
tags:
- project
summary: Create project invitation
parameters:
- name: Invite
in: body
required: true
schema:
$ref: "#/definitions/ProjectInviteRequest"
responses:
201:
description: Invitation created
schema:
$ref: "#/definitions/ProjectInvite"
400:
description: Bad request (invalid role, missing user_id/email, or both provided)
409:
description: User already a member or invitation already exists

/project/{project_id}/invites/{invite_id}:
parameters:
- $ref: "#/parameters/project_id"
- $ref: "#/parameters/invite_id"
get:
tags:
- project
summary: Get specific project invitation
responses:
200:
description: Project invitation
schema:
$ref: "#/definitions/ProjectInvite"
404:
description: Invitation not found
# put:
# tags:
# - project
# summary: Update project invitation status
# parameters:
# - name: Invite Update
# in: body
# required: true
# schema:
# type: object
# properties:
# status:
# type: string
# enum: [pending, declined, expired]
# example: declined
# responses:
# 204:
# description: Invitation updated
# 400:
# description: Invalid status or status transition
# delete:
# tags:
# - project
# summary: Delete project invitation
# responses:
# 204:
# description: Invitation deleted
#
# /invites/accept:
# post:
# tags:
# - project
# summary: Accept project invitation
# parameters:
# - name: Accept Invite
# in: body
# required: true
# schema:
# $ref: "#/definitions/AcceptInviteRequest"
# responses:
# 204:
# description: Invitation accepted successfully
# 400:
# description: Invalid token, invitation expired, or user already a member
# 403:
# description: Invitation not for this user
# 404:
# description: Invitation not found

/project/{project_id}/integrations:
parameters:
- $ref: "#/parameters/project_id"
Expand Down
Loading
Loading