feat: add initial implementation of Go utilities and CI workflows

This commit is contained in:
2025-06-18 16:46:01 -04:00
commit c588ade142
12 changed files with 475 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
on:
push:
tags:
- "v*"
name: 🚀 Publish Release Package
jobs:
publish:
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: 📥 Checkout code
uses: actions/checkout@v2
- name: Install gopack
run: |
curl -L https://github.com/cloudsmith-io/gopack/releases/download/v0.6.0/gopack_0.6.0_linux_amd64.tar.gz -o /tmp/gopack.tar.gz
tar -xzf /tmp/gopack.tar.gz -C /tmp
mv /tmp/gopack /usr/local/bin/gopack
- name: 📦 Build Go Package
run: |
gopack ${{ github.ref_name }} .
- name: 📦 Publish Build Artifacts
uses: christopherhx/gitea-upload-artifact@v4
with:
name: ${{ github.ref_name }}
path: ${{ github.ref_name }}.zip
retention-days: 1
- name: ☁️ Upload release package
run: |
curl --user ${{ secrets.PACKAGE_PUBLISH_USER }}:${{ secrets.PACKAGE_PUBLISH_TOKEN }} \
--upload-file ${{ github.ref_name }}.zip \
${{ gitea.server_url }}/api/packages/packages/go/upload
if [ $? -ne 0 ]; then
echo "Error uploading release package"
exit 1
fi
echo "Upload successful"

View File

@@ -0,0 +1,77 @@
on:
workflow_dispatch: {}
push:
branches:
- "*"
name: 🧪 ✨ Tests Workflow
env:
GO_VERSION: '1.24.3'
jobs:
lint-go:
name: 🔍 🐹 Go Lint
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: ⚙️ 🐹 Set up Go Environment
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: 📦 📥 Install Dependencies
run: |
go mod download
- name: ✅ 🔍 Run Go Lint
uses: golangci/golangci-lint-action@v8.0.0
test-go:
name: 🔍 🐹 Go Tests
runs-on: ubuntu-latest
steps:
- name: 🛡️ 🔒 Add Siteworx CA Certificates
run: |
curl -Ls https://siteworxpro.com/hosted/Siteworx+Root+CA.pem -o /usr/local/share/ca-certificates/sw.crt
update-ca-certificates
- name: ⚙️ 🐹 Set up Go Environment
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: 📖 🔍 Checkout Repository Code
uses: actions/checkout@v2
with:
fetch-depth: 1
- name: 📦 📥 Install Dependencies
run: |
go mod download
- name: ✅ 🔍 Run Go Tests
run: |
go test -v ./... -coverprofile=coverage.out
- name: 📊 📈 Upload Coverage Report
env:
NODE_TLS_REJECT_UNAUTHORIZED: 0
uses: christopherhx/gitea-upload-artifact@v4
with:
name: coverage-report
path: coverage.out
retention-days: 7

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea/

26
.golangci.yml Normal file
View File

@@ -0,0 +1,26 @@
version: "2"
linters:
default: standard
enable:
- whitespace
- tagalign
- reassign
- bodyclose
- contextcheck
- containedctx
- godot
- usestdlibvars
- gochecknoglobals
- tagalign
- sqlclosecheck
- rowserrcheck
- recvcheck
- reassign
- predeclared
- maintidx
- mnd
formatters:
settings:
gofmt:
simplify: true

55
Env/env.go Executable file
View File

@@ -0,0 +1,55 @@
package Env
import (
"log"
"os"
"strconv"
"strings"
)
type EnvironmentVariable string
func (v EnvironmentVariable) GetName() string {
return string(v)
}
func (v EnvironmentVariable) GetEnvString(fallback string) string {
if value, ok := os.LookupEnv(string(v)); ok {
return value
}
return fallback
}
func (v EnvironmentVariable) GetEnvBool(fallback bool) bool {
value, ok := os.LookupEnv(string(v))
if !ok {
return fallback
}
value = strings.ToLower(value)
trueValues := []string{"1", "true"}
for _, x := range trueValues {
if value == x {
return true
}
}
return false
}
func (v EnvironmentVariable) GetEnvInt(key string, fallback int64) int64 {
if value, ok := os.LookupEnv(key); ok {
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
log.Fatal(err)
}
return i
}
return fallback
}

69
Env/env_test.go Normal file
View File

@@ -0,0 +1,69 @@
package Env
import (
"os"
"testing"
)
func TestEnvironmentVariable_GetName(t *testing.T) {
v := EnvironmentVariable("TEST_VAR")
if v.GetName() != "TEST_VAR" {
t.Errorf("expected 'TEST_VAR', got '%s'", v.GetName())
}
}
func TestEnvironmentVariable_GetEnvString(t *testing.T) {
const envName = "TEST_STRING"
_ = os.Setenv(envName, "value")
defer func() {
_ = os.Unsetenv(envName)
}()
v := EnvironmentVariable(envName)
got := v.GetEnvString("fallback")
if got != "value" {
t.Errorf("expected 'value', got '%s'", got)
}
_ = os.Unsetenv(envName)
got = v.GetEnvString("fallback")
if got != "fallback" {
t.Errorf("expected 'fallback', got '%s'", got)
}
}
func TestEnvironmentVariable_GetEnvBool(t *testing.T) {
const envName = "TEST_BOOL"
v := EnvironmentVariable(envName)
_ = os.Setenv(envName, "true")
if !v.GetEnvBool(false) {
t.Errorf("expected true for 'true'")
}
_ = os.Setenv(envName, "1")
if !v.GetEnvBool(false) {
t.Errorf("expected true for '1'")
}
_ = os.Setenv(envName, "false")
if v.GetEnvBool(true) {
t.Errorf("expected false for 'false'")
}
_ = os.Unsetenv(envName)
if v.GetEnvBool(true) != true {
t.Errorf("expected fallback true")
}
}
func TestEnvironmentVariable_GetEnvInt(t *testing.T) {
const envName = "TEST_INT"
v := EnvironmentVariable(envName)
_ = os.Setenv(envName, "42")
if v.GetEnvInt(envName, 10) != 42 {
t.Errorf("expected 42")
}
_ = os.Unsetenv(envName)
if v.GetEnvInt(envName, 10) != 10 {
t.Errorf("expected fallback 10")
}
}

11
Maps/maps.go Executable file
View File

@@ -0,0 +1,11 @@
package Maps
//goland:noinspection GoUnusedExportedFunction // library function
func IsElementExist(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}

26
Maps/maps_test.go Normal file
View File

@@ -0,0 +1,26 @@
package Maps
import "testing"
func TestIsElementExist(t *testing.T) {
tests := []struct {
name string
s []string
str string
expected bool
}{
{"element exists", []string{"a", "b", "c"}, "b", true},
{"element does not exist", []string{"a", "b", "c"}, "d", false},
{"empty slice", []string{}, "a", false},
{"multiple same elements", []string{"a", "b", "a"}, "a", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsElementExist(tt.s, tt.str)
if result != tt.expected {
t.Errorf("IsElementExist(%v, %q) = %v; want %v", tt.s, tt.str, result, tt.expected)
}
})
}
}

1
README.md Executable file
View File

@@ -0,0 +1 @@
# Utilities

114
Yubikey/otp.go Executable file
View File

@@ -0,0 +1,114 @@
package Yubikey
import (
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
type Response struct {
h string
t time.Time
otp string
presentedNonce string
nonce string
sl uint8
timestamp uint32
sessionCounter uint32
sessionUse uint32
status string
}
const keyLen = 40
func (response *Response) Time() time.Time {
return response.t
}
func (response *Response) TimeStamp() uint32 {
return response.timestamp
}
func (response *Response) Valid() bool {
return response.status == "OK" && response.nonce == response.presentedNonce
}
//goland:noinspection GoUnusedExportedFunction // library function
func GetVerification(otp string, clientId string) (*Response, error) {
nonce := randSeq(keyLen)
url := fmt.Sprintf(
"https://api.yubico.com/wsapi/2.0/verify?id=%s&otp=%s&timestamp=1&nonce=%s",
clientId,
otp,
nonce,
)
response, _ := http.Get(url)
defer func() {
_ = response.Body.Close()
}()
body := make([]byte, response.ContentLength)
_, err := response.Body.Read(body)
if err != nil {
log.Fatal(err)
}
parts := strings.Split(string(body), "\r\n")
var status string
for k, part := range parts {
if strings.Contains(part, "status=") {
status = strings.Replace(parts[k], "status=", "", 1)
break
}
}
if status != "OK" {
return nil, errors.New("invalid response status: " + status)
}
timeString := strings.Replace(parts[1], "t=", "", 1)[0:20]
signedTime, err := time.Parse(time.RFC3339, timeString)
if err != nil {
println(err)
}
sl, _ := strconv.ParseInt(strings.Replace(parts[4], "sl=", "", 1), 10, 8)
timestamp, _ := strconv.ParseInt(strings.Replace(parts[5], "timestamp=", "", 1), 10, 32)
sessionCounter, _ := strconv.ParseInt(strings.Replace(parts[6], "sessioncounter=", "", 1), 10, 32)
sessionUse, _ := strconv.ParseInt(strings.Replace(parts[7], "sessionuse=", "", 1), 10, 32)
keyResponse := Response{
h: strings.Replace(parts[0], "h=", "", 1),
t: signedTime,
otp: strings.Replace(parts[2], "otp=", "", 1),
presentedNonce: nonce,
nonce: strings.Replace(parts[3], "nonce=", "", 1),
sl: uint8(sl),
timestamp: uint32(timestamp),
sessionCounter: uint32(sessionCounter),
sessionUse: uint32(sessionUse),
status: status,
}
return &keyResponse, nil
}
func randSeq(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

44
Yubikey/otp_test.go Normal file
View File

@@ -0,0 +1,44 @@
package Yubikey
import (
"testing"
"time"
)
// Test the Valid method of Response.
func TestResponse_Valid(t *testing.T) {
resp := &Response{
status: "OK",
nonce: "abc123",
presentedNonce: "abc123",
}
if !resp.Valid() {
t.Errorf("Expected Valid() to return true")
}
resp.status = "BAD"
if resp.Valid() {
t.Errorf("Expected Valid() to return false when status is not OK")
}
resp.status = "OK"
resp.nonce = "xyz"
if resp.Valid() {
t.Errorf("Expected Valid() to return false when nonce does not match presentedNonce")
}
}
// Test the Time and TimeStamp methods.
func TestResponse_TimeAndTimeStamp(t *testing.T) {
now := time.Now()
resp := &Response{
t: now,
timestamp: 12345,
}
if !resp.Time().Equal(now) {
t.Errorf("Expected Time() to return the correct time")
}
if resp.TimeStamp() != 12345 {
t.Errorf("Expected TimeStamp() to return 12345")
}
}

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module gitea.siteworxpro.com/packages/golang-utilities
go 1.23.4