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
12 changes: 6 additions & 6 deletions backend/plugins/bitbucket/api/connection_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,22 +206,22 @@ func TestMergeFromRequest_HandlesUsesApiToken(t *testing.T) {
// After merge, UsesApiToken should be updated
// This is a structural test - actual merge logic is in the connection.go MergeFromRequest method
assert.True(t, connection.UsesApiToken, "Initial value should be true")

// If we were to apply the merge:
connection.UsesApiToken = newValues["usesApiToken"].(bool)
connection.Username = newValues["username"].(string)

assert.False(t, connection.UsesApiToken, "After merge, should be false")
assert.Equal(t, "new_username", connection.Username)
}

func TestConnectionStatusCodes(t *testing.T) {
// Test expected status code handling
tests := []struct {
name string
statusCode int
expectedError bool
errorType string
name string
statusCode int
expectedError bool
errorType string
}{
{
name: "Success - 200 OK",
Expand Down
81 changes: 81 additions & 0 deletions backend/plugins/claude_code/api/blueprint_v200.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"github.com/apache/incubator-devlake/core/errors"
coreModels "github.com/apache/incubator-devlake/core/models"
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/helpers/srvhelper"
"github.com/apache/incubator-devlake/plugins/claude_code/models"
"github.com/apache/incubator-devlake/plugins/claude_code/tasks"
)

// MakeDataSourcePipelinePlanV200 generates the pipeline plan for blueprint v2.0.0.
func MakeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
connectionId uint64,
bpScopes []*coreModels.BlueprintScope,
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
_, err := dsHelper.ConnSrv.FindByPk(connectionId)
if err != nil {
return nil, nil, err
}
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
if err != nil {
return nil, nil, err
}

plan, err := makeDataSourcePipelinePlanV200(subtaskMetas, scopeDetails)
if err != nil {
return nil, nil, err
}

return plan, nil, nil
}

func makeDataSourcePipelinePlanV200(
subtaskMetas []plugin.SubTaskMeta,
scopeDetails []*srvhelper.ScopeDetail[models.ClaudeCodeScope, models.ClaudeCodeScopeConfig],
) (coreModels.PipelinePlan, errors.Error) {
plan := make(coreModels.PipelinePlan, len(scopeDetails))
for i, scopeDetail := range scopeDetails {
stage := plan[i]
if stage == nil {
stage = coreModels.PipelineStage{}
}

scope := scopeDetail.Scope
task, err := helper.MakePipelinePlanTask(
"claude_code",
subtaskMetas,
nil,
tasks.ClaudeCodeOptions{
ConnectionId: scope.ConnectionId,
ScopeId: scope.Id,
},
)
if err != nil {
return nil, err
}
stage = append(stage, task)
plan[i] = stage
}
return plan, nil
}
113 changes: 113 additions & 0 deletions backend/plugins/claude_code/api/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"strings"

"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
"github.com/apache/incubator-devlake/plugins/claude_code/models"
)

// PostConnections creates a new Claude Code connection.
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.ClaudeCodeConnection{}
if err := helper.Decode(input.Body, connection, vld); err != nil {
return nil, err
}

connection.Normalize()
if err := validateConnection(connection); err != nil {
return nil, err
}

if err := connectionHelper.Create(connection, input); err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
}

func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.ClaudeCodeConnection{}
if err := connectionHelper.First(connection, input.Params); err != nil {
return nil, err
}
if err := (&models.ClaudeCodeConnection{}).MergeFromRequest(connection, input.Body); err != nil {
return nil, errors.Convert(err)
}
connection.Normalize()
if err := validateConnection(connection); err != nil {
return nil, err
}
if err := connectionHelper.SaveWithCreateOrUpdate(connection); err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
}

func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
conn := &models.ClaudeCodeConnection{}
output, err := connectionHelper.Delete(conn, input)
if err != nil {
return output, err
}
output.Body = conn.Sanitize()
return output, nil
}

func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
var connections []models.ClaudeCodeConnection
if err := connectionHelper.List(&connections); err != nil {
return nil, err
}
for i := range connections {
connections[i] = connections[i].Sanitize()
}
return &plugin.ApiResourceOutput{Body: connections}, nil
}

func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
connection := &models.ClaudeCodeConnection{}
if err := connectionHelper.First(connection, input.Params); err != nil {
return nil, err
}
return &plugin.ApiResourceOutput{Body: connection.Sanitize()}, nil
}

func validateConnection(connection *models.ClaudeCodeConnection) errors.Error {
if connection == nil {
return errors.BadInput.New("connection is required")
}
hasToken := strings.TrimSpace(connection.Token) != ""
if connection.HasIncompleteCustomHeaders() {
return errors.BadInput.New("custom headers must include both key and value")
}
hasCustomHeaders := connection.HasUsableCustomHeaders()
if !hasToken && !hasCustomHeaders {
return errors.BadInput.New("either token or at least one custom header is required")
}
if strings.TrimSpace(connection.Organization) == "" {
return errors.BadInput.New("organization is required")
}
Comment thread
la-tamas marked this conversation as resolved.
if connection.RateLimitPerHour < 0 {
return errors.BadInput.New("rateLimitPerHour must be non-negative")
}
return nil
}
135 changes: 135 additions & 0 deletions backend/plugins/claude_code/api/connection_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/apache/incubator-devlake/plugins/claude_code/models"
)

const (
testOrganization = "anthropic-labs"
testToken = "sk-ant-example"
)

func TestValidateConnectionSuccess(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
Token: testToken,
},
}
connection.Normalize()

err := validateConnection(connection)
assert.NoError(t, err)
}

func TestValidateConnectionNil(t *testing.T) {
err := validateConnection(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "connection is required")
}

func TestValidateConnectionMissingOrganization(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Token: testToken,
},
}

err := validateConnection(connection)
assert.Error(t, err)
assert.Contains(t, err.Error(), "organization is required")
}

func TestValidateConnectionMissingToken(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
},
}

err := validateConnection(connection)
assert.Error(t, err)
assert.Contains(t, err.Error(), "either token or at least one custom header is required")
}

func TestValidateConnectionCustomHeadersWithoutToken(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
CustomHeaders: []models.CustomHeader{
{Key: "Ocp-Apim-Subscription-Key", Value: "secret-key"},
},
},
}
connection.Normalize()

err := validateConnection(connection)
assert.NoError(t, err)
}

func TestValidateConnectionRejectsBlankCustomHeaders(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
CustomHeaders: []models.CustomHeader{
{Key: "", Value: ""},
},
},
}
connection.Normalize()

err := validateConnection(connection)
assert.Error(t, err)
assert.Contains(t, err.Error(), "either token or at least one custom header is required")
}

func TestValidateConnectionRejectsIncompleteCustomHeaders(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
CustomHeaders: []models.CustomHeader{
{Key: "Ocp-Apim-Subscription-Key", Value: ""},
},
},
}
connection.Normalize()

err := validateConnection(connection)
assert.Error(t, err)
assert.Contains(t, err.Error(), "custom headers must include both key and value")
}

func TestValidateConnectionInvalidRateLimit(t *testing.T) {
connection := &models.ClaudeCodeConnection{
ClaudeCodeConn: models.ClaudeCodeConn{
Organization: testOrganization,
Token: testToken,
},
}
connection.RateLimitPerHour = -1

err := validateConnection(connection)
assert.Error(t, err)
assert.Contains(t, err.Error(), "rateLimitPerHour must be non-negative")
}
Loading
Loading