• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 25997704812

17 May 2026 05:26PM UTC coverage: 76.463% (+0.03%) from 76.429%
25997704812

push

github

web-flow
Safety improvements (file operations, etc.) (#17)

283 of 349 new or added lines in 42 files covered. (81.09%)

2 existing lines in 1 file now uncovered.

44229 of 57844 relevant lines covered (76.46%)

34600.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.05
/internal/api/nodes/putnode/handler.go
1
package putnode
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "net/http"
7
        "path/filepath"
8
        "time"
9

10
        "github.com/gameap/gameap/internal/api/base"
11
        "github.com/gameap/gameap/internal/domain"
12
        "github.com/gameap/gameap/internal/files"
13
        "github.com/gameap/gameap/internal/filters"
14
        "github.com/gameap/gameap/internal/repositories"
15
        "github.com/gameap/gameap/pkg/api"
16
        "github.com/gameap/gameap/pkg/secret"
17
        "github.com/pkg/errors"
18
        "github.com/rs/xid"
19
)
20

21
const certificatesPath = "certs"
22

23
var (
24
        ErrFailedToSaveCertificate = errors.New("failed to save certificate")
25
        ErrFailedToUpdateNode      = errors.New("failed to update node")
26
        ErrNodeNotFound            = errors.New("node not found")
27
)
28

29
type Handler struct {
30
        repo        repositories.NodeRepository
31
        fileManager files.FileManager
32
        cipher      *secret.Cipher
33
        responder   base.Responder
34
}
35

36
func NewHandler(
37
        repo repositories.NodeRepository,
38
        fileManager files.FileManager,
39
        cipher *secret.Cipher,
40
        responder base.Responder,
41
) *Handler {
17✔
42
        return &Handler{
17✔
43
                repo:        repo,
17✔
44
                fileManager: fileManager,
17✔
45
                cipher:      cipher,
17✔
46
                responder:   responder,
17✔
47
        }
17✔
48
}
17✔
49

50
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
17✔
51
        ctx := r.Context()
17✔
52

17✔
53
        input := api.NewInputReader(r)
17✔
54

17✔
55
        nodeID, err := input.ReadUint("id")
17✔
56
        if err != nil {
17✔
57
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
×
58
                        errors.WithMessage(err, "invalid node id"),
×
59
                        http.StatusBadRequest,
×
60
                ))
×
61

×
62
                return
×
63
        }
×
64

65
        node, err := h.findNode(ctx, nodeID)
17✔
66
        if err != nil {
19✔
67
                h.responder.WriteError(ctx, rw, err)
2✔
68

2✔
69
                return
2✔
70
        }
2✔
71

72
        var updateInput updateNodeInput
15✔
73
        err = json.NewDecoder(r.Body).Decode(&updateInput)
15✔
74
        if err != nil {
15✔
75
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
×
76
                        errors.WithMessage(err, "invalid request"),
×
77
                        http.StatusBadRequest,
×
78
                ))
×
79

×
80
                return
×
81
        }
×
82

83
        err = updateInput.Validate()
15✔
84
        if err != nil {
19✔
85
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
4✔
86
                        errors.WithMessage(err, "validation failed"),
4✔
87
                        http.StatusUnprocessableEntity,
4✔
88
                ))
4✔
89

4✔
90
                return
4✔
91
        }
4✔
92

93
        updatedNode, err := h.updateNode(ctx, node, &updateInput)
11✔
94
        if err != nil {
12✔
95
                h.responder.WriteError(ctx, rw, errors.WithMessage(err, "failed to update node"))
1✔
96

1✔
97
                return
1✔
98
        }
1✔
99

100
        response := newNodeResponse(updatedNode)
10✔
101
        h.responder.Write(ctx, rw, response)
10✔
102
}
103

104
func (h *Handler) findNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
17✔
105
        nodes, err := h.repo.Find(ctx, &filters.FindNode{
17✔
106
                IDs: []uint{nodeID},
17✔
107
        }, nil, &filters.Pagination{
17✔
108
                Limit:  1,
17✔
109
                Offset: 0,
17✔
110
        })
17✔
111
        if err != nil {
17✔
112
                return nil, errors.WithMessage(err, "failed to find node")
×
113
        }
×
114

115
        if len(nodes) == 0 {
19✔
116
                return nil, api.WrapHTTPError(ErrNodeNotFound, http.StatusNotFound)
2✔
117
        }
2✔
118

119
        return &nodes[0], nil
15✔
120
}
121

122
func (h *Handler) updateNode(ctx context.Context, node *domain.Node, input *updateNodeInput) (*domain.Node, error) {
11✔
123
        oldCertPath := node.GdaemonServerCert
11✔
124

11✔
125
        if input.GdaemonServerCert != nil && *input.GdaemonServerCert != "" {
15✔
126
                certXID := xid.New().String()
4✔
127
                certPath := filepath.Join(certificatesPath, certXID+".crt")
4✔
128

4✔
129
                if err := h.fileManager.Write(ctx, certPath, []byte(*input.GdaemonServerCert)); err != nil {
5✔
130
                        return nil, errors.WithMessage(ErrFailedToSaveCertificate, err.Error())
1✔
131
                }
1✔
132

133
                node.GdaemonServerCert = certPath
3✔
134
        }
135

136
        input.ApplyToNode(node)
10✔
137

10✔
138
        // Encrypt the SSH password at rest whenever the field is supplied in this
10✔
139
        // request. A request that omits gdaemon_password leaves the stored value
10✔
140
        // untouched; one that includes it is re-encrypted even if byte-identical,
10✔
141
        // so the ciphertext rotates (GCM uses a fresh nonce per call). Node PUTs
10✔
142
        // are rare admin operations, so this write churn is acceptable.
10✔
143
        if input.GdaemonPassword != nil && node.GdaemonPassword != nil {
13✔
144
                encrypted, err := h.cipher.Encrypt(*node.GdaemonPassword)
3✔
145
                if err != nil {
3✔
NEW
146
                        return nil, errors.WithMessage(err, "failed to encrypt gdaemon password")
×
NEW
147
                }
×
148

149
                node.GdaemonPassword = &encrypted
3✔
150
        }
151

152
        now := time.Now()
10✔
153
        node.UpdatedAt = &now
10✔
154

10✔
155
        if err := h.repo.Save(ctx, node); err != nil {
10✔
156
                if input.GdaemonServerCert != nil && *input.GdaemonServerCert != "" {
×
157
                        _ = h.fileManager.Delete(ctx, node.GdaemonServerCert)
×
158
                }
×
159

160
                return nil, errors.WithMessage(ErrFailedToUpdateNode, err.Error())
×
161
        }
162

163
        if input.GdaemonServerCert != nil && *input.GdaemonServerCert != "" && oldCertPath != "" {
13✔
164
                _ = h.fileManager.Delete(ctx, oldCertPath)
3✔
165
        }
3✔
166

167
        return node, nil
10✔
168
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc