Compare commits
3 Commits
main
...
46255bb1a2
| Author | SHA1 | Date | |
|---|---|---|---|
| 46255bb1a2 | |||
| bbd4909fc8 | |||
| 988adf6cc5 |
115
.github/workflows/ci.yml
vendored
115
.github/workflows/ci.yml
vendored
@@ -35,28 +35,19 @@ jobs:
|
|||||||
|
|
||||||
- name: Install protoc and plugins
|
- name: Install protoc and plugins
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache protobuf-dev protoc
|
apt-get update
|
||||||
|
apt-get install -y protobuf-compiler
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
echo "$HOME/go/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Generate code
|
- name: Generate protobuf files
|
||||||
|
run: make generate-proto
|
||||||
|
|
||||||
|
- name: Generate Ent ORM files
|
||||||
run: |
|
run: |
|
||||||
make generate-proto
|
go install entgo.io/ent/cmd/ent@latest
|
||||||
echo "Checking for Ent schema directory..."
|
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
|
||||||
if [ -d "ent/schema" ]; then
|
cd ent && go generate ./...
|
||||||
echo "Generating Ent code..."
|
|
||||||
go install entgo.io/ent/cmd/ent@latest
|
|
||||||
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
|
|
||||||
echo "Copying Ent code to internal/ent..."
|
|
||||||
cd .. && mkdir -p ../internal/ent
|
|
||||||
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
|
|
||||||
rm -f ../internal/ent/generate.go
|
|
||||||
rm -rf ../internal/ent/schema
|
|
||||||
echo "Verifying internal/ent/ent.go exists..."
|
|
||||||
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
|
|
||||||
else
|
|
||||||
echo "WARNING: ent/schema directory not found!"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Check for test files
|
- name: Check for test files
|
||||||
@@ -77,7 +68,7 @@ jobs:
|
|||||||
if: steps.check-tests.outputs.tests_exist == 'true'
|
if: steps.check-tests.outputs.tests_exist == 'true'
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
run: make test-coverage
|
run: go test -v -race -coverprofile=coverage.out -timeout=5m ./...
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
if: steps.check-tests.outputs.tests_exist == 'true'
|
if: steps.check-tests.outputs.tests_exist == 'true'
|
||||||
@@ -88,7 +79,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Verify build (no tests)
|
- name: Verify build (no tests)
|
||||||
if: steps.check-tests.outputs.tests_exist == 'false'
|
if: steps.check-tests.outputs.tests_exist == 'false'
|
||||||
run: make build
|
run: |
|
||||||
|
echo "No tests found. Verifying code compiles instead..."
|
||||||
|
go build ./...
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
@@ -107,38 +100,26 @@ jobs:
|
|||||||
|
|
||||||
- name: Install protoc and plugins
|
- name: Install protoc and plugins
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache protobuf-dev protoc
|
apt-get update
|
||||||
|
apt-get install -y protobuf-compiler
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
echo "$HOME/go/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Generate code
|
- name: Generate protobuf files
|
||||||
|
run: make generate-proto
|
||||||
|
|
||||||
|
- name: Generate Ent ORM files
|
||||||
run: |
|
run: |
|
||||||
make generate-proto
|
go install entgo.io/ent/cmd/ent@latest
|
||||||
echo "Checking for Ent schema directory..."
|
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
|
||||||
if [ -d "ent/schema" ]; then
|
cd ent && go generate ./...
|
||||||
echo "Generating Ent code..."
|
|
||||||
go install entgo.io/ent/cmd/ent@latest
|
|
||||||
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
|
|
||||||
echo "Copying Ent code to internal/ent..."
|
|
||||||
cd .. && mkdir -p ../internal/ent
|
|
||||||
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
|
|
||||||
rm -f ../internal/ent/generate.go
|
|
||||||
rm -rf ../internal/ent/schema
|
|
||||||
echo "Verifying internal/ent/ent.go exists..."
|
|
||||||
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
|
|
||||||
else
|
|
||||||
echo "WARNING: ent/schema directory not found!"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Install golangci-lint
|
- name: golangci-lint
|
||||||
run: |
|
uses: golangci/golangci-lint-action@v7
|
||||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
|
with:
|
||||||
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
version: latest
|
||||||
golangci-lint --version
|
args: --timeout=5m
|
||||||
|
|
||||||
- name: Run linters
|
|
||||||
run: make lint
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@@ -165,32 +146,25 @@ jobs:
|
|||||||
|
|
||||||
- name: Install protoc and plugins
|
- name: Install protoc and plugins
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache protobuf-dev protoc
|
apt-get update
|
||||||
|
apt-get install -y protobuf-compiler
|
||||||
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
||||||
echo "$HOME/go/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Generate code
|
- name: Generate protobuf files
|
||||||
|
run: make generate-proto
|
||||||
|
|
||||||
|
- name: Generate Ent ORM files
|
||||||
run: |
|
run: |
|
||||||
make generate-proto
|
go install entgo.io/ent/cmd/ent@latest
|
||||||
echo "Checking for Ent schema directory..."
|
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
|
||||||
if [ -d "ent/schema" ]; then
|
cd ent && go generate ./...
|
||||||
echo "Generating Ent code..."
|
|
||||||
go install entgo.io/ent/cmd/ent@latest
|
|
||||||
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
|
|
||||||
echo "Copying Ent code to internal/ent..."
|
|
||||||
cd .. && mkdir -p ../internal/ent
|
|
||||||
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
|
|
||||||
rm -f ../internal/ent/generate.go
|
|
||||||
rm -rf ../internal/ent/schema
|
|
||||||
echo "Verifying internal/ent/ent.go exists..."
|
|
||||||
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
|
|
||||||
else
|
|
||||||
echo "WARNING: ent/schema directory not found!"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build
|
run: |
|
||||||
|
go build -v -o bin/platform ./cmd/platform
|
||||||
|
go build -v -o bin/api-gateway ./cmd/api-gateway
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -199,10 +173,6 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
bin/platform
|
bin/platform
|
||||||
bin/api-gateway
|
bin/api-gateway
|
||||||
bin/auth-service
|
|
||||||
bin/identity-service
|
|
||||||
bin/authz-service
|
|
||||||
bin/audit-service
|
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@@ -218,4 +188,9 @@ jobs:
|
|||||||
go-version: '1.25.3'
|
go-version: '1.25.3'
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
run: make fmt-check
|
run: |
|
||||||
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
|
echo "The following files need formatting:"
|
||||||
|
gofmt -s -d .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -69,9 +69,6 @@ Thumbs.db
|
|||||||
# Generated protobuf files
|
# Generated protobuf files
|
||||||
api/proto/generated/
|
api/proto/generated/
|
||||||
|
|
||||||
# Generated Ent ORM files (but keep schema source files)
|
# Generated Ent ORM files
|
||||||
internal/ent/
|
internal/ent/
|
||||||
ent/*.go
|
ent/
|
||||||
ent/*/
|
|
||||||
!ent/schema/
|
|
||||||
git.dcentral.systems/
|
|
||||||
12
AGENTS.md
12
AGENTS.md
@@ -197,14 +197,10 @@ When working on this project, follow this workflow:
|
|||||||
|
|
||||||
### 7. Commit Changes
|
### 7. Commit Changes
|
||||||
- **ALWAYS commit** after successful implementation
|
- **ALWAYS commit** after successful implementation
|
||||||
- Verify that everything is in order before commit:
|
- Ensure the code builds (`go build`)
|
||||||
- there is a Gitea Runner image in ci/pre-commit
|
- Ensure all tests pass (`go test`)
|
||||||
- run scripts/pre-commit-check.sh
|
- Ensure there are no linter issues (`make lint`)
|
||||||
- Ensure the code builds (`make build`)
|
- Ensure there are no fmt issues (`make fmt-check`)
|
||||||
- Ensure all tests pass (`make test`)
|
|
||||||
- Ensure there are no linter issues (`make lint`)
|
|
||||||
- Ensure there are no fmt issues (`make fmt-check`)
|
|
||||||
- If there are issues, fix them before comitting
|
|
||||||
- Verify all acceptance criteria are met
|
- Verify all acceptance criteria are met
|
||||||
- Write a clear, descriptive commit message
|
- Write a clear, descriptive commit message
|
||||||
|
|
||||||
|
|||||||
19
Makefile
19
Makefile
@@ -18,7 +18,7 @@ help:
|
|||||||
@echo " make lint - Run linters"
|
@echo " make lint - Run linters"
|
||||||
@echo " make fmt - Format code"
|
@echo " make fmt - Format code"
|
||||||
@echo " make fmt-check - Check code formatting"
|
@echo " make fmt-check - Check code formatting"
|
||||||
@echo " make build - Build all service binaries"
|
@echo " make build - Build platform and api-gateway binaries"
|
||||||
@echo " make clean - Clean build artifacts"
|
@echo " make clean - Clean build artifacts"
|
||||||
@echo " make docker-build - Build Docker image"
|
@echo " make docker-build - Build Docker image"
|
||||||
@echo " make docker-run - Run Docker container"
|
@echo " make docker-run - Run Docker container"
|
||||||
@@ -85,14 +85,10 @@ fmt-check:
|
|||||||
@echo "Code is properly formatted"
|
@echo "Code is properly formatted"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@echo "Building all service binaries..."
|
@echo "Building platform and api-gateway binaries..."
|
||||||
$(GO) build -v -o bin/platform ./cmd/platform
|
$(GO) build -v -o bin/platform ./cmd/platform
|
||||||
$(GO) build -v -o bin/api-gateway ./cmd/api-gateway
|
$(GO) build -v -o bin/api-gateway ./cmd/api-gateway
|
||||||
$(GO) build -v -o bin/auth-service ./cmd/auth-service
|
@echo "Build complete: bin/platform, bin/api-gateway"
|
||||||
$(GO) build -v -o bin/identity-service ./cmd/identity-service
|
|
||||||
$(GO) build -v -o bin/authz-service ./cmd/authz-service
|
|
||||||
$(GO) build -v -o bin/audit-service ./cmd/audit-service
|
|
||||||
@echo "Build complete: bin/platform, bin/api-gateway, bin/auth-service, bin/identity-service, bin/authz-service, bin/audit-service"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@@ -134,27 +130,18 @@ generate-proto:
|
|||||||
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
||||||
--proto_path=api/proto \
|
--proto_path=api/proto \
|
||||||
api/proto/audit.proto
|
api/proto/audit.proto
|
||||||
@if [ -f api/proto/generated/audit.pb.go ]; then mv api/proto/generated/audit.pb.go api/proto/generated/audit/v1/audit.pb.go; fi
|
|
||||||
@if [ -f api/proto/generated/audit_grpc.pb.go ]; then mv api/proto/generated/audit_grpc.pb.go api/proto/generated/audit/v1/audit_grpc.pb.go; fi
|
|
||||||
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
||||||
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
||||||
--proto_path=api/proto \
|
--proto_path=api/proto \
|
||||||
api/proto/auth.proto
|
api/proto/auth.proto
|
||||||
@if [ -f api/proto/generated/auth.pb.go ]; then mv api/proto/generated/auth.pb.go api/proto/generated/auth/v1/auth.pb.go; fi
|
|
||||||
@if [ -f api/proto/generated/auth_grpc.pb.go ]; then mv api/proto/generated/auth_grpc.pb.go api/proto/generated/auth/v1/auth_grpc.pb.go; fi
|
|
||||||
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
||||||
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
||||||
--proto_path=api/proto \
|
--proto_path=api/proto \
|
||||||
api/proto/authz.proto
|
api/proto/authz.proto
|
||||||
@if [ -f api/proto/generated/authz.pb.go ]; then mv api/proto/generated/authz.pb.go api/proto/generated/authz/v1/authz.pb.go; fi
|
|
||||||
@if [ -f api/proto/generated/authz_grpc.pb.go ]; then mv api/proto/generated/authz_grpc.pb.go api/proto/generated/authz/v1/authz_grpc.pb.go; fi
|
|
||||||
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
@protoc --go_out=api/proto/generated --go_opt=paths=source_relative \
|
||||||
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
--go-grpc_out=api/proto/generated --go-grpc_opt=paths=source_relative \
|
||||||
--proto_path=api/proto \
|
--proto_path=api/proto \
|
||||||
api/proto/identity.proto
|
api/proto/identity.proto
|
||||||
@if [ -f api/proto/generated/identity.pb.go ]; then mv api/proto/generated/identity.pb.go api/proto/generated/identity/v1/identity.pb.go; fi
|
|
||||||
@if [ -f api/proto/generated/identity_grpc.pb.go ]; then mv api/proto/generated/identity_grpc.pb.go api/proto/generated/identity/v1/identity_grpc.pb.go; fi
|
|
||||||
@rm -f api/proto/generated/*.pb.go api/proto/generated/*.proto 2>/dev/null || true
|
|
||||||
@echo "gRPC code generation complete"
|
@echo "gRPC code generation complete"
|
||||||
|
|
||||||
verify: fmt-check lint test
|
verify: fmt-check lint test
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apk add --no-cache \
|
|
||||||
nodejs \
|
|
||||||
npm \
|
|
||||||
gcc \
|
|
||||||
build-base \
|
|
||||||
musl-dev \
|
|
||||||
curl \
|
|
||||||
make \
|
|
||||||
wget \
|
|
||||||
tar \
|
|
||||||
bash \
|
|
||||||
git \
|
|
||||||
protobuf \
|
|
||||||
protobuf-dev
|
|
||||||
|
|
||||||
# Install Go 1.25.3
|
|
||||||
RUN cd /tmp && \
|
|
||||||
wget -q https://go.dev/dl/go1.25.3.linux-amd64.tar.gz && \
|
|
||||||
tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz && \
|
|
||||||
rm go1.25.3.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
# Set up Go environment
|
|
||||||
ENV PATH=/usr/local/go/bin:$PATH:/root/go/bin
|
|
||||||
ENV GOROOT=/usr/local/go
|
|
||||||
ENV GOPATH=/root/go
|
|
||||||
ENV CGO_ENABLED=1
|
|
||||||
ENV GOFLAGS=-buildvcs=false
|
|
||||||
|
|
||||||
# Install Go protobuf plugins
|
|
||||||
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
|
|
||||||
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
|
|
||||||
|
|
||||||
# Install golangci-lint
|
|
||||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /root/go/bin
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /workspace
|
|
||||||
@@ -5,7 +5,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -276,16 +275,9 @@ func (s *auditServerImpl) Query(ctx context.Context, req *auditv1.QueryRequest)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
total := len(protoEntries)
|
|
||||||
var totalInt32 int32
|
|
||||||
if total > math.MaxInt32 {
|
|
||||||
totalInt32 = math.MaxInt32
|
|
||||||
} else {
|
|
||||||
totalInt32 = int32(total)
|
|
||||||
}
|
|
||||||
return &auditv1.QueryResponse{
|
return &auditv1.QueryResponse{
|
||||||
Entries: protoEntries,
|
Entries: protoEntries,
|
||||||
Total: totalInt32,
|
Total: int32(len(protoEntries)),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -73,17 +72,7 @@ func verifyPassword(password, hash string) (bool, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
hashLen := len(expectedHash)
|
actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, uint32(len(expectedHash)))
|
||||||
if hashLen < 0 || hashLen > math.MaxUint32 {
|
|
||||||
return false, fmt.Errorf("invalid hash length: %d", hashLen)
|
|
||||||
}
|
|
||||||
var hashLenUint32 uint32
|
|
||||||
if hashLen > math.MaxUint32 {
|
|
||||||
hashLenUint32 = math.MaxUint32
|
|
||||||
} else {
|
|
||||||
hashLenUint32 = uint32(hashLen)
|
|
||||||
}
|
|
||||||
actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, hashLenUint32)
|
|
||||||
return subtle.ConstantTimeCompare(expectedHash, actualHash) == 1, nil
|
return subtle.ConstantTimeCompare(expectedHash, actualHash) == 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
// Package schema defines the Ent schema for domain entities.
|
|
||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
"entgo.io/ent/schema/index"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AuditLog holds the schema definition for the AuditLog entity.
|
|
||||||
type AuditLog struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the AuditLog.
|
|
||||||
func (AuditLog) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("user_id").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("ID of the user/actor performing the action"),
|
|
||||||
field.String("action").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("Action performed (e.g., user.create, user.update)"),
|
|
||||||
field.String("resource").
|
|
||||||
Optional().
|
|
||||||
Comment("Resource type (e.g., user, role)"),
|
|
||||||
field.String("resource_id").
|
|
||||||
Optional().
|
|
||||||
Comment("ID of the target resource"),
|
|
||||||
field.String("ip_address").
|
|
||||||
Optional().
|
|
||||||
Comment("IP address of the client"),
|
|
||||||
field.String("user_agent").
|
|
||||||
Optional().
|
|
||||||
Comment("User agent of the client"),
|
|
||||||
field.JSON("metadata", map[string]interface{}{}).
|
|
||||||
Optional().
|
|
||||||
Comment("Additional metadata as JSON"),
|
|
||||||
field.Time("timestamp").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indexes of the AuditLog.
|
|
||||||
func (AuditLog) Indexes() []ent.Index {
|
|
||||||
return []ent.Index{
|
|
||||||
index.Fields("user_id"),
|
|
||||||
index.Fields("resource_id"),
|
|
||||||
index.Fields("timestamp"),
|
|
||||||
index.Fields("action"),
|
|
||||||
index.Fields("resource"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permission holds the schema definition for the Permission entity.
|
|
||||||
type Permission struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the Permission.
|
|
||||||
func (Permission) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("name").
|
|
||||||
Unique().
|
|
||||||
NotEmpty().
|
|
||||||
Comment("Format: module.resource.action"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the Permission.
|
|
||||||
func (Permission) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role_permissions", RolePermission.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
"entgo.io/ent/schema/index"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RefreshToken holds the schema definition for the RefreshToken entity.
|
|
||||||
type RefreshToken struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the RefreshToken.
|
|
||||||
func (RefreshToken) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("user_id").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("ID of the user who owns this refresh token"),
|
|
||||||
field.String("token_hash").
|
|
||||||
NotEmpty().
|
|
||||||
Sensitive().
|
|
||||||
Comment("SHA256 hash of the refresh token"),
|
|
||||||
field.Time("expires_at").
|
|
||||||
Comment("When the refresh token expires"),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indexes of the RefreshToken.
|
|
||||||
func (RefreshToken) Indexes() []ent.Index {
|
|
||||||
return []ent.Index{
|
|
||||||
index.Fields("user_id"),
|
|
||||||
index.Fields("token_hash"),
|
|
||||||
index.Fields("expires_at"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Role holds the schema definition for the Role entity.
|
|
||||||
type Role struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the Role.
|
|
||||||
func (Role) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("name").
|
|
||||||
Unique().
|
|
||||||
NotEmpty(),
|
|
||||||
field.String("description").
|
|
||||||
Optional(),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the Role.
|
|
||||||
func (Role) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role_permissions", RolePermission.Type),
|
|
||||||
edge.To("user_roles", UserRole.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RolePermission holds the schema definition for the RolePermission entity (many-to-many relationship).
|
|
||||||
type RolePermission struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the RolePermission.
|
|
||||||
func (RolePermission) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("role_id"),
|
|
||||||
field.String("permission_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the RolePermission.
|
|
||||||
func (RolePermission) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role", Role.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("role_id"),
|
|
||||||
edge.To("permission", Permission.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("permission_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User holds the schema definition for the User entity.
|
|
||||||
type User struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the User.
|
|
||||||
func (User) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("email").
|
|
||||||
Unique().
|
|
||||||
NotEmpty(),
|
|
||||||
field.String("username").
|
|
||||||
Optional(),
|
|
||||||
field.String("first_name").
|
|
||||||
Optional(),
|
|
||||||
field.String("last_name").
|
|
||||||
Optional(),
|
|
||||||
field.String("password_hash").
|
|
||||||
NotEmpty(),
|
|
||||||
field.Bool("verified").
|
|
||||||
Default(false),
|
|
||||||
field.String("email_verification_token").
|
|
||||||
Optional().
|
|
||||||
Sensitive(),
|
|
||||||
field.String("password_reset_token").
|
|
||||||
Optional().
|
|
||||||
Sensitive(),
|
|
||||||
field.Time("password_reset_expires_at").
|
|
||||||
Optional(),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
field.Time("updated_at").
|
|
||||||
Default(time.Now).
|
|
||||||
UpdateDefault(time.Now),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the User.
|
|
||||||
func (User) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("user_roles", UserRole.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserRole holds the schema definition for the UserRole entity (many-to-many relationship).
|
|
||||||
type UserRole struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the UserRole.
|
|
||||||
func (UserRole) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("user_id"),
|
|
||||||
field.String("role_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the UserRole.
|
|
||||||
func (UserRole) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("user", User.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("user_id"),
|
|
||||||
edge.To("role", Role.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("role_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ package grpc
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
|
|
||||||
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
|
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
||||||
@@ -87,24 +86,9 @@ func (c *AuditClient) Query(ctx context.Context, filters *services.AuditLogFilte
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var limitInt32, offsetInt32 int32
|
|
||||||
if filters.Limit > math.MaxInt32 {
|
|
||||||
limitInt32 = math.MaxInt32
|
|
||||||
} else if filters.Limit < math.MinInt32 {
|
|
||||||
limitInt32 = math.MinInt32
|
|
||||||
} else {
|
|
||||||
limitInt32 = int32(filters.Limit) //nolint:gosec // bounds checked above
|
|
||||||
}
|
|
||||||
if filters.Offset > math.MaxInt32 {
|
|
||||||
offsetInt32 = math.MaxInt32
|
|
||||||
} else if filters.Offset < math.MinInt32 {
|
|
||||||
offsetInt32 = math.MinInt32
|
|
||||||
} else {
|
|
||||||
offsetInt32 = int32(filters.Offset) //nolint:gosec // bounds checked above
|
|
||||||
}
|
|
||||||
req := &auditv1.QueryRequest{
|
req := &auditv1.QueryRequest{
|
||||||
Limit: limitInt32,
|
Limit: int32(filters.Limit),
|
||||||
Offset: offsetInt32,
|
Offset: int32(filters.Offset),
|
||||||
}
|
}
|
||||||
|
|
||||||
if filters.UserID != nil {
|
if filters.UserID != nil {
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ func (c *IdentityClient) VerifyPassword(ctx context.Context, email, password str
|
|||||||
|
|
||||||
resp, err := c.client.VerifyPassword(ctx, &identityv1.VerifyPasswordRequest{
|
resp, err := c.client.VerifyPassword(ctx, &identityv1.VerifyPasswordRequest{
|
||||||
Email: email,
|
Email: email,
|
||||||
Password: password,
|
Password: password,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("verify password failed: %w", err)
|
return nil, fmt.Errorf("verify password failed: %w", err)
|
||||||
|
|||||||
@@ -100,15 +100,9 @@ func LoadConfig(env string) (config.ConfigProvider, error) {
|
|||||||
// e.g., DATABASE_DSN -> database.dsn, SERVER_PORT -> server.port
|
// e.g., DATABASE_DSN -> database.dsn, SERVER_PORT -> server.port
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
// Bind specific environment variables to config keys
|
// Bind specific environment variables to config keys
|
||||||
if err := v.BindEnv("database.dsn", "DATABASE_DSN"); err != nil {
|
v.BindEnv("database.dsn", "DATABASE_DSN")
|
||||||
return nil, fmt.Errorf("failed to bind DATABASE_DSN: %w", err)
|
v.BindEnv("registry.consul.address", "REGISTRY_CONSUL_ADDRESS")
|
||||||
}
|
v.BindEnv("registry.type", "REGISTRY_TYPE")
|
||||||
if err := v.BindEnv("registry.consul.address", "REGISTRY_CONSUL_ADDRESS"); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to bind REGISTRY_CONSUL_ADDRESS: %w", err)
|
|
||||||
}
|
|
||||||
if err := v.BindEnv("registry.type", "REGISTRY_TYPE"); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to bind REGISTRY_TYPE: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewViperConfig(v), nil
|
return NewViperConfig(v), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
// Package schema defines the Ent schema for domain entities.
|
|
||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
"entgo.io/ent/schema/index"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AuditLog holds the schema definition for the AuditLog entity.
|
|
||||||
type AuditLog struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the AuditLog.
|
|
||||||
func (AuditLog) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("user_id").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("ID of the user/actor performing the action"),
|
|
||||||
field.String("action").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("Action performed (e.g., user.create, user.update)"),
|
|
||||||
field.String("resource").
|
|
||||||
Optional().
|
|
||||||
Comment("Resource type (e.g., user, role)"),
|
|
||||||
field.String("resource_id").
|
|
||||||
Optional().
|
|
||||||
Comment("ID of the target resource"),
|
|
||||||
field.String("ip_address").
|
|
||||||
Optional().
|
|
||||||
Comment("IP address of the client"),
|
|
||||||
field.String("user_agent").
|
|
||||||
Optional().
|
|
||||||
Comment("User agent of the client"),
|
|
||||||
field.JSON("metadata", map[string]interface{}{}).
|
|
||||||
Optional().
|
|
||||||
Comment("Additional metadata as JSON"),
|
|
||||||
field.Time("timestamp").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indexes of the AuditLog.
|
|
||||||
func (AuditLog) Indexes() []ent.Index {
|
|
||||||
return []ent.Index{
|
|
||||||
index.Fields("user_id"),
|
|
||||||
index.Fields("resource_id"),
|
|
||||||
index.Fields("timestamp"),
|
|
||||||
index.Fields("action"),
|
|
||||||
index.Fields("resource"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permission holds the schema definition for the Permission entity.
|
|
||||||
type Permission struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the Permission.
|
|
||||||
func (Permission) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("name").
|
|
||||||
Unique().
|
|
||||||
NotEmpty().
|
|
||||||
Comment("Format: module.resource.action"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the Permission.
|
|
||||||
func (Permission) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role_permissions", RolePermission.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
"entgo.io/ent/schema/index"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RefreshToken holds the schema definition for the RefreshToken entity.
|
|
||||||
type RefreshToken struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the RefreshToken.
|
|
||||||
func (RefreshToken) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("user_id").
|
|
||||||
NotEmpty().
|
|
||||||
Comment("ID of the user who owns this refresh token"),
|
|
||||||
field.String("token_hash").
|
|
||||||
NotEmpty().
|
|
||||||
Sensitive().
|
|
||||||
Comment("SHA256 hash of the refresh token"),
|
|
||||||
field.Time("expires_at").
|
|
||||||
Comment("When the refresh token expires"),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indexes of the RefreshToken.
|
|
||||||
func (RefreshToken) Indexes() []ent.Index {
|
|
||||||
return []ent.Index{
|
|
||||||
index.Fields("user_id"),
|
|
||||||
index.Fields("token_hash"),
|
|
||||||
index.Fields("expires_at"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Role holds the schema definition for the Role entity.
|
|
||||||
type Role struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the Role.
|
|
||||||
func (Role) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("name").
|
|
||||||
Unique().
|
|
||||||
NotEmpty(),
|
|
||||||
field.String("description").
|
|
||||||
Optional(),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the Role.
|
|
||||||
func (Role) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role_permissions", RolePermission.Type),
|
|
||||||
edge.To("user_roles", UserRole.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RolePermission holds the schema definition for the RolePermission entity (many-to-many relationship).
|
|
||||||
type RolePermission struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the RolePermission.
|
|
||||||
func (RolePermission) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("role_id"),
|
|
||||||
field.String("permission_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the RolePermission.
|
|
||||||
func (RolePermission) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("role", Role.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("role_id"),
|
|
||||||
edge.To("permission", Permission.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("permission_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User holds the schema definition for the User entity.
|
|
||||||
type User struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the User.
|
|
||||||
func (User) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("id").
|
|
||||||
Unique().
|
|
||||||
Immutable(),
|
|
||||||
field.String("email").
|
|
||||||
Unique().
|
|
||||||
NotEmpty(),
|
|
||||||
field.String("username").
|
|
||||||
Optional(),
|
|
||||||
field.String("first_name").
|
|
||||||
Optional(),
|
|
||||||
field.String("last_name").
|
|
||||||
Optional(),
|
|
||||||
field.String("password_hash").
|
|
||||||
NotEmpty(),
|
|
||||||
field.Bool("verified").
|
|
||||||
Default(false),
|
|
||||||
field.String("email_verification_token").
|
|
||||||
Optional().
|
|
||||||
Sensitive(),
|
|
||||||
field.String("password_reset_token").
|
|
||||||
Optional().
|
|
||||||
Sensitive(),
|
|
||||||
field.Time("password_reset_expires_at").
|
|
||||||
Optional(),
|
|
||||||
field.Time("created_at").
|
|
||||||
Default(time.Now).
|
|
||||||
Immutable(),
|
|
||||||
field.Time("updated_at").
|
|
||||||
Default(time.Now).
|
|
||||||
UpdateDefault(time.Now),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the User.
|
|
||||||
func (User) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("user_roles", UserRole.Type),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"entgo.io/ent"
|
|
||||||
"entgo.io/ent/schema/edge"
|
|
||||||
"entgo.io/ent/schema/field"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UserRole holds the schema definition for the UserRole entity (many-to-many relationship).
|
|
||||||
type UserRole struct {
|
|
||||||
ent.Schema
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields of the UserRole.
|
|
||||||
func (UserRole) Fields() []ent.Field {
|
|
||||||
return []ent.Field{
|
|
||||||
field.String("user_id"),
|
|
||||||
field.String("role_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Edges of the UserRole.
|
|
||||||
func (UserRole) Edges() []ent.Edge {
|
|
||||||
return []ent.Edge{
|
|
||||||
edge.To("user", User.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("user_id"),
|
|
||||||
edge.To("role", Role.Type).
|
|
||||||
Unique().
|
|
||||||
Required().
|
|
||||||
Field("role_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Pre-commit check script that runs lint, fmt-check, test, and build in gitea-runner container
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
IMAGE_NAME="wirelos/pre-commit"
|
|
||||||
CONTAINER_NAME="goplt-pre-commit-check"
|
|
||||||
|
|
||||||
echo "🔍 Checking for Docker image: $IMAGE_NAME"
|
|
||||||
if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${IMAGE_NAME}:latest$"; then
|
|
||||||
echo "📦 Image not found. Building $IMAGE_NAME from ci/pre-commit/Dockerfile..."
|
|
||||||
docker build -t "$IMAGE_NAME:latest" -f "$PROJECT_ROOT/ci/pre-commit/Dockerfile" "$PROJECT_ROOT/ci/pre-commit" || {
|
|
||||||
echo "❌ Failed to build Docker image"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ Image built successfully"
|
|
||||||
else
|
|
||||||
echo "✅ Image found locally"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🧹 Cleaning up any existing container..."
|
|
||||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
|
||||||
|
|
||||||
echo "🚀 Starting pre-commit container..."
|
|
||||||
docker run --rm \
|
|
||||||
--name "$CONTAINER_NAME" \
|
|
||||||
-v "$PROJECT_ROOT:/workspace" \
|
|
||||||
-w /workspace \
|
|
||||||
"$IMAGE_NAME:latest" \
|
|
||||||
sh -c "
|
|
||||||
echo '📋 Running make fmt-check...'
|
|
||||||
make fmt-check || exit 1
|
|
||||||
|
|
||||||
echo '🔍 Running make lint...'
|
|
||||||
make lint || exit 1
|
|
||||||
|
|
||||||
echo '🧪 Running make test...'
|
|
||||||
make test || exit 1
|
|
||||||
|
|
||||||
echo '🔨 Running make build...'
|
|
||||||
make build || exit 1
|
|
||||||
|
|
||||||
echo '✅ All checks passed!'
|
|
||||||
"
|
|
||||||
|
|
||||||
EXIT_CODE=$?
|
|
||||||
|
|
||||||
if [ $EXIT_CODE -eq 0 ]; then
|
|
||||||
echo "✅ All pre-commit checks passed!"
|
|
||||||
else
|
|
||||||
echo "❌ Pre-commit checks failed. Please fix the issues above."
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit $EXIT_CODE
|
|
||||||
|
|
||||||
@@ -3,7 +3,6 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"math"
|
|
||||||
|
|
||||||
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
|
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
|
||||||
"git.dcentral.systems/toolz/goplt/services/audit/internal/service"
|
"git.dcentral.systems/toolz/goplt/services/audit/internal/service"
|
||||||
@@ -119,15 +118,8 @@ func (s *Server) Query(ctx context.Context, req *auditv1.QueryRequest) (*auditv1
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
total := len(protoEntries)
|
|
||||||
var totalInt32 int32
|
|
||||||
if total > math.MaxInt32 {
|
|
||||||
totalInt32 = math.MaxInt32
|
|
||||||
} else {
|
|
||||||
totalInt32 = int32(total)
|
|
||||||
}
|
|
||||||
return &auditv1.QueryResponse{
|
return &auditv1.QueryResponse{
|
||||||
Entries: protoEntries,
|
Entries: protoEntries,
|
||||||
Total: totalInt32, // Note: This is a simplified total, actual total would require a count query
|
Total: int32(len(protoEntries)), // Note: This is a simplified total, actual total would require a count query
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGateway_pathMatches(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestPath string
|
|
||||||
routePath string
|
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "exact match",
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
routePath: "/api/v1/auth",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exact match with trailing slash",
|
|
||||||
requestPath: "/api/v1/auth/",
|
|
||||||
routePath: "/api/v1/auth",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard match - prefix",
|
|
||||||
requestPath: "/api/v1/auth/login",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard match - exact prefix",
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard match - nested path",
|
|
||||||
requestPath: "/api/v1/auth/refresh/token",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no match - different prefix",
|
|
||||||
requestPath: "/api/v1/users",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no match - exact",
|
|
||||||
requestPath: "/api/v1/users",
|
|
||||||
routePath: "/api/v1/auth",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard no match - wrong prefix",
|
|
||||||
requestPath: "/api/v1/users/login",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gateway := &Gateway{}
|
|
||||||
got := gateway.pathMatches(tt.requestPath, tt.routePath)
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_extractRemainingPath(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestPath string
|
|
||||||
routePath string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "wildcard - extract path",
|
|
||||||
requestPath: "/api/v1/auth/login",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: "/login",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard - extract nested path",
|
|
||||||
requestPath: "/api/v1/auth/refresh/token",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: "/refresh/token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard - exact match",
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exact match - no remaining",
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
routePath: "/api/v1/auth",
|
|
||||||
want: "/",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no match - empty",
|
|
||||||
requestPath: "/api/v1/users",
|
|
||||||
routePath: "/api/v1/auth/**",
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gateway := &Gateway{}
|
|
||||||
got := gateway.extractRemainingPath(tt.requestPath, tt.routePath)
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_matchRoute(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
routes []RouteConfig
|
|
||||||
requestPath string
|
|
||||||
want *RouteConfig
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "exact match",
|
|
||||||
routes: []RouteConfig{
|
|
||||||
{Path: "/api/v1/auth", Service: "auth-service"},
|
|
||||||
{Path: "/api/v1/users", Service: "identity-service"},
|
|
||||||
},
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
want: &RouteConfig{Path: "/api/v1/auth", Service: "auth-service"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard match",
|
|
||||||
routes: []RouteConfig{
|
|
||||||
{Path: "/api/v1/auth/**", Service: "auth-service"},
|
|
||||||
},
|
|
||||||
requestPath: "/api/v1/auth/login",
|
|
||||||
want: &RouteConfig{Path: "/api/v1/auth/**", Service: "auth-service"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no match",
|
|
||||||
routes: []RouteConfig{
|
|
||||||
{Path: "/api/v1/auth/**", Service: "auth-service"},
|
|
||||||
},
|
|
||||||
requestPath: "/api/v1/other",
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty routes",
|
|
||||||
routes: []RouteConfig{},
|
|
||||||
requestPath: "/api/v1/auth",
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gateway := &Gateway{
|
|
||||||
routes: tt.routes,
|
|
||||||
}
|
|
||||||
got := gateway.matchRoute(tt.requestPath)
|
|
||||||
if tt.want == nil {
|
|
||||||
assert.Nil(t, got)
|
|
||||||
} else {
|
|
||||||
assert.NotNil(t, got)
|
|
||||||
assert.Equal(t, tt.want.Path, got.Path)
|
|
||||||
assert.Equal(t, tt.want.Service, got.Service)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"git.dcentral.systems/toolz/goplt/pkg/config"
|
"git.dcentral.systems/toolz/goplt/pkg/config"
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
"git.dcentral.systems/toolz/goplt/pkg/registry"
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/services"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -90,7 +89,7 @@ func TestGateway_SetupRoutes(t *testing.T) {
|
|||||||
{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
|
{Path: "/api/v1/test", Service: "test-service", AuthRequired: false},
|
||||||
},
|
},
|
||||||
requestPath: "/api/v1/test",
|
requestPath: "/api/v1/test",
|
||||||
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
expectedStatus: http.StatusServiceUnavailable, // Will fail service discovery
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no routes configured",
|
name: "no routes configured",
|
||||||
@@ -152,7 +151,7 @@ func TestGateway_handleRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestPath: "/api/v1/test",
|
requestPath: "/api/v1/test",
|
||||||
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
expectedStatus: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no service instances",
|
name: "no service instances",
|
||||||
@@ -166,7 +165,7 @@ func TestGateway_handleRoute(t *testing.T) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestPath: "/api/v1/test",
|
requestPath: "/api/v1/test",
|
||||||
expectedStatus: http.StatusNotFound, // Unknown service returns 404
|
expectedStatus: http.StatusServiceUnavailable,
|
||||||
},
|
},
|
||||||
// Note: Testing invalid target URL and proxy forwarding requires integration tests
|
// Note: Testing invalid target URL and proxy forwarding requires integration tests
|
||||||
// with real HTTP servers, as httptest.ResponseRecorder doesn't support CloseNotify
|
// with real HTTP servers, as httptest.ResponseRecorder doesn't support CloseNotify
|
||||||
@@ -224,7 +223,7 @@ func TestGateway_handleRoute_AllMethods(t *testing.T) {
|
|||||||
clientFactory: &client.ServiceClientFactory{},
|
clientFactory: &client.ServiceClientFactory{},
|
||||||
registry: mockReg,
|
registry: mockReg,
|
||||||
routes: []RouteConfig{
|
routes: []RouteConfig{
|
||||||
{Path: "/api/v1/test/**", Service: "test-service"}, // Use wildcard pattern
|
{Path: "/api/v1/test", Service: "test-service"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +237,6 @@ func TestGateway_handleRoute_AllMethods(t *testing.T) {
|
|||||||
router.ServeHTTP(w, req)
|
router.ServeHTTP(w, req)
|
||||||
|
|
||||||
// All methods should be handled (route should match, even if service unavailable)
|
// All methods should be handled (route should match, even if service unavailable)
|
||||||
// New implementation returns 500 for unknown services, not 404
|
|
||||||
assert.NotEqual(t, http.StatusNotFound, w.Code, "Route should match for %s", method)
|
assert.NotEqual(t, http.StatusNotFound, w.Code, "Route should match for %s", method)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -426,87 +424,3 @@ func (m *mockServiceRegistry) Health(ctx context.Context, serviceID string) (*re
|
|||||||
Status: "healthy",
|
Status: "healthy",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockAuthClient implements services.AuthServiceClient for testing.
|
|
||||||
type mockAuthClient struct {
|
|
||||||
loginResp *services.TokenResponse
|
|
||||||
loginErr error
|
|
||||||
refreshResp *services.TokenResponse
|
|
||||||
refreshErr error
|
|
||||||
validateResp *services.TokenClaims
|
|
||||||
validateErr error
|
|
||||||
logoutErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAuthClient) Login(ctx context.Context, email, password string) (*services.TokenResponse, error) {
|
|
||||||
return m.loginResp, m.loginErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAuthClient) RefreshToken(ctx context.Context, refreshToken string) (*services.TokenResponse, error) {
|
|
||||||
return m.refreshResp, m.refreshErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAuthClient) ValidateToken(ctx context.Context, token string) (*services.TokenClaims, error) {
|
|
||||||
return m.validateResp, m.validateErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockAuthClient) Logout(ctx context.Context, refreshToken string) error {
|
|
||||||
return m.logoutErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockIdentityClient implements services.IdentityServiceClient for testing.
|
|
||||||
type mockIdentityClient struct {
|
|
||||||
getUserResp *services.User
|
|
||||||
getUserErr error
|
|
||||||
getUserByEmailResp *services.User
|
|
||||||
getUserByEmailErr error
|
|
||||||
createUserResp *services.User
|
|
||||||
createUserErr error
|
|
||||||
updateUserResp *services.User
|
|
||||||
updateUserErr error
|
|
||||||
deleteUserErr error
|
|
||||||
verifyEmailErr error
|
|
||||||
requestPasswordResetErr error
|
|
||||||
resetPasswordErr error
|
|
||||||
verifyPasswordResp *services.User
|
|
||||||
verifyPasswordErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) GetUser(ctx context.Context, id string) (*services.User, error) {
|
|
||||||
return m.getUserResp, m.getUserErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) GetUserByEmail(ctx context.Context, email string) (*services.User, error) {
|
|
||||||
return m.getUserByEmailResp, m.getUserByEmailErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) CreateUser(ctx context.Context, req *services.CreateUserRequest) (*services.User, error) {
|
|
||||||
return m.createUserResp, m.createUserErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) UpdateUser(ctx context.Context, id string, req *services.UpdateUserRequest) (*services.User, error) {
|
|
||||||
return m.updateUserResp, m.updateUserErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) DeleteUser(ctx context.Context, id string) error {
|
|
||||||
return m.deleteUserErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) VerifyEmail(ctx context.Context, token string) error {
|
|
||||||
return m.verifyEmailErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) RequestPasswordReset(ctx context.Context, email string) error {
|
|
||||||
return m.requestPasswordResetErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) ResetPassword(ctx context.Context, token, newPassword string) error {
|
|
||||||
return m.resetPasswordErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockIdentityClient) VerifyPassword(ctx context.Context, email, password string) (*services.User, error) {
|
|
||||||
return m.verifyPasswordResp, m.verifyPasswordErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: mockClientFactory is not needed since we're testing handlers directly with mock clients.
|
|
||||||
// The actual clientFactory is tested separately in integration tests.
|
|
||||||
|
|||||||
@@ -1,519 +0,0 @@
|
|||||||
package gateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.dcentral.systems/toolz/goplt/pkg/services"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGateway_handleLogin(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody interface{}
|
|
||||||
clientResp *services.TokenResponse
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
expectedBody func(*testing.T, *httptest.ResponseRecorder)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful login",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"email": "test@example.com",
|
|
||||||
"password": "password123",
|
|
||||||
},
|
|
||||||
clientResp: &services.TokenResponse{
|
|
||||||
AccessToken: "access-token",
|
|
||||||
RefreshToken: "refresh-token",
|
|
||||||
ExpiresIn: 3600,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
},
|
|
||||||
clientErr: nil,
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
expectedBody: func(t *testing.T, w *httptest.ResponseRecorder) {
|
|
||||||
var resp services.TokenResponse
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "access-token", resp.AccessToken)
|
|
||||||
assert.Equal(t, "refresh-token", resp.RefreshToken)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid request body",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"email": "test@example.com",
|
|
||||||
// missing password
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error - unauthorized",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"email": "test@example.com",
|
|
||||||
"password": "wrongpassword",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.Unauthenticated, "invalid credentials"),
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error - internal",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"email": "test@example.com",
|
|
||||||
"password": "password123",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.Internal, "internal error"),
|
|
||||||
expectedStatus: http.StatusInternalServerError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockAuthClient{
|
|
||||||
loginResp: tt.clientResp,
|
|
||||||
loginErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/login", func(c *gin.Context) {
|
|
||||||
gateway.handleLogin(context.Background(), c, mockClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewReader(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
if tt.expectedBody != nil {
|
|
||||||
tt.expectedBody(t, w)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleRefreshToken(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody interface{}
|
|
||||||
clientResp *services.TokenResponse
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful refresh",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"refresh_token": "refresh-token-123",
|
|
||||||
},
|
|
||||||
clientResp: &services.TokenResponse{
|
|
||||||
AccessToken: "new-access-token",
|
|
||||||
RefreshToken: "new-refresh-token",
|
|
||||||
ExpiresIn: 3600,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid request body",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
// missing refresh_token
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"refresh_token": "invalid-token",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.Unauthenticated, "invalid token"),
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockAuthClient{
|
|
||||||
refreshResp: tt.clientResp,
|
|
||||||
refreshErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/refresh", func(c *gin.Context) {
|
|
||||||
gateway.handleRefreshToken(context.Background(), c, mockClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/refresh", bytes.NewReader(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleValidateToken(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody interface{}
|
|
||||||
clientResp *services.TokenClaims
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful validation",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"token": "valid-token",
|
|
||||||
},
|
|
||||||
clientResp: &services.TokenClaims{
|
|
||||||
UserID: "user-123",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Roles: []string{"user"},
|
|
||||||
ExpiresAt: 1234567890,
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid request body",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
// missing token
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error - invalid token",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"token": "invalid-token",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.Unauthenticated, "invalid token"),
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockAuthClient{
|
|
||||||
validateResp: tt.clientResp,
|
|
||||||
validateErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/validate", func(c *gin.Context) {
|
|
||||||
gateway.handleValidateToken(context.Background(), c, mockClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/validate", bytes.NewReader(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleLogout(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody interface{}
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful logout",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"refresh_token": "refresh-token-123",
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid request body",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
// missing refresh_token
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error",
|
|
||||||
requestBody: map[string]string{
|
|
||||||
"refresh_token": "invalid-token",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.NotFound, "token not found"),
|
|
||||||
expectedStatus: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockAuthClient{
|
|
||||||
logoutErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/logout", func(c *gin.Context) {
|
|
||||||
gateway.handleLogout(context.Background(), c, mockClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/logout", bytes.NewReader(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleGetUser(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
userID string
|
|
||||||
clientResp *services.User
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful get user",
|
|
||||||
userID: "user-123",
|
|
||||||
clientResp: &services.User{
|
|
||||||
ID: "user-123",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Username: "testuser",
|
|
||||||
FirstName: "Test",
|
|
||||||
LastName: "User",
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusOK,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error - not found",
|
|
||||||
userID: "nonexistent",
|
|
||||||
clientErr: status.Error(codes.NotFound, "user not found"),
|
|
||||||
expectedStatus: http.StatusNotFound,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockIdentityClient{
|
|
||||||
getUserResp: tt.clientResp,
|
|
||||||
getUserErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.GET("/users/:id", func(c *gin.Context) {
|
|
||||||
gateway.handleGetUser(context.Background(), c, mockClient, tt.userID)
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/users/"+tt.userID, nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleCreateUser(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
requestBody interface{}
|
|
||||||
clientResp *services.User
|
|
||||||
clientErr error
|
|
||||||
expectedStatus int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "successful create",
|
|
||||||
requestBody: services.CreateUserRequest{
|
|
||||||
Email: "test@example.com",
|
|
||||||
Username: "testuser",
|
|
||||||
Password: "password123",
|
|
||||||
FirstName: "Test",
|
|
||||||
LastName: "User",
|
|
||||||
},
|
|
||||||
clientResp: &services.User{
|
|
||||||
ID: "user-123",
|
|
||||||
Email: "test@example.com",
|
|
||||||
Username: "testuser",
|
|
||||||
FirstName: "Test",
|
|
||||||
LastName: "User",
|
|
||||||
},
|
|
||||||
expectedStatus: http.StatusCreated,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid JSON",
|
|
||||||
requestBody: "not a json object",
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "client error - already exists",
|
|
||||||
requestBody: services.CreateUserRequest{
|
|
||||||
Email: "existing@example.com",
|
|
||||||
Username: "existing",
|
|
||||||
Password: "password123",
|
|
||||||
},
|
|
||||||
clientErr: status.Error(codes.AlreadyExists, "user already exists"),
|
|
||||||
expectedStatus: http.StatusConflict,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
mockClient := &mockIdentityClient{
|
|
||||||
createUserResp: tt.clientResp,
|
|
||||||
createUserErr: tt.clientErr,
|
|
||||||
}
|
|
||||||
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.POST("/users", func(c *gin.Context) {
|
|
||||||
gateway.handleCreateUser(context.Background(), c, mockClient)
|
|
||||||
})
|
|
||||||
|
|
||||||
bodyBytes, _ := json.Marshal(tt.requestBody)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(bodyBytes))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGateway_handleGRPCError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedStatus int
|
|
||||||
expectedError string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Unauthenticated",
|
|
||||||
err: status.Error(codes.Unauthenticated, "invalid token"),
|
|
||||||
expectedStatus: http.StatusUnauthorized,
|
|
||||||
expectedError: "Unauthorized",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "PermissionDenied",
|
|
||||||
err: status.Error(codes.PermissionDenied, "access denied"),
|
|
||||||
expectedStatus: http.StatusForbidden,
|
|
||||||
expectedError: "Forbidden",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "NotFound",
|
|
||||||
err: status.Error(codes.NotFound, "resource not found"),
|
|
||||||
expectedStatus: http.StatusNotFound,
|
|
||||||
expectedError: "Not found",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "InvalidArgument",
|
|
||||||
err: status.Error(codes.InvalidArgument, "invalid input"),
|
|
||||||
expectedStatus: http.StatusBadRequest,
|
|
||||||
expectedError: "Invalid request",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "AlreadyExists",
|
|
||||||
err: status.Error(codes.AlreadyExists, "resource exists"),
|
|
||||||
expectedStatus: http.StatusConflict,
|
|
||||||
expectedError: "Resource already exists",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Internal",
|
|
||||||
err: status.Error(codes.Internal, "internal error"),
|
|
||||||
expectedStatus: http.StatusInternalServerError,
|
|
||||||
expectedError: "Internal server error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Unavailable",
|
|
||||||
err: status.Error(codes.Unavailable, "service unavailable"),
|
|
||||||
expectedStatus: http.StatusServiceUnavailable,
|
|
||||||
expectedError: "Service unavailable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-gRPC error",
|
|
||||||
err: assert.AnError,
|
|
||||||
expectedStatus: http.StatusInternalServerError,
|
|
||||||
expectedError: "Internal server error",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gateway := &Gateway{
|
|
||||||
log: &mockLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
router := gin.New()
|
|
||||||
router.GET("/test", func(c *gin.Context) {
|
|
||||||
gateway.handleGRPCError(c, tt.err)
|
|
||||||
})
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(w, req)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
|
||||||
|
|
||||||
var response map[string]interface{}
|
|
||||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.expectedError, response["error"])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
@@ -78,44 +77,7 @@ func Verify(password, hash string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute hash with same parameters
|
// Compute hash with same parameters
|
||||||
hashLen := len(expectedHash)
|
actualHash := argon2.IDKey([]byte(password), salt, uint32(t), uint32(m), uint8(p), uint32(len(expectedHash)))
|
||||||
if hashLen < 0 || hashLen > math.MaxUint32 {
|
|
||||||
return false, fmt.Errorf("invalid hash length: %d", hashLen)
|
|
||||||
}
|
|
||||||
var hashLenUint32 uint32
|
|
||||||
if hashLen > math.MaxUint32 {
|
|
||||||
hashLenUint32 = math.MaxUint32
|
|
||||||
} else {
|
|
||||||
hashLenUint32 = uint32(hashLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds check for t and m to prevent overflow
|
|
||||||
var tUint32, mUint32 uint32
|
|
||||||
if t > math.MaxUint32 {
|
|
||||||
tUint32 = math.MaxUint32
|
|
||||||
} else if t < 0 {
|
|
||||||
tUint32 = 0
|
|
||||||
} else {
|
|
||||||
tUint32 = uint32(t) //nolint:gosec // bounds checked above
|
|
||||||
}
|
|
||||||
if m > math.MaxUint32 {
|
|
||||||
mUint32 = math.MaxUint32
|
|
||||||
} else if m < 0 {
|
|
||||||
mUint32 = 0
|
|
||||||
} else {
|
|
||||||
mUint32 = uint32(m) //nolint:gosec // bounds checked above
|
|
||||||
}
|
|
||||||
|
|
||||||
var pUint8 uint8
|
|
||||||
if p > math.MaxUint8 {
|
|
||||||
pUint8 = math.MaxUint8
|
|
||||||
} else if p < 0 {
|
|
||||||
pUint8 = 0
|
|
||||||
} else {
|
|
||||||
pUint8 = uint8(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
actualHash := argon2.IDKey([]byte(password), salt, tUint32, mUint32, pUint8, hashLenUint32)
|
|
||||||
|
|
||||||
// Constant-time comparison
|
// Constant-time comparison
|
||||||
if subtle.ConstantTimeCompare(expectedHash, actualHash) == 1 {
|
if subtle.ConstantTimeCompare(expectedHash, actualHash) == 1 {
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
package password
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestHash(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
password string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid password",
|
|
||||||
password: "testpassword123",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty password",
|
|
||||||
password: "",
|
|
||||||
wantErr: false, // Empty password is valid, just hashed
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "long password",
|
|
||||||
password: "this is a very long password with many characters and symbols !@#$%^&*()",
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
hash, err := Hash(tt.password)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Empty(t, hash)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, hash)
|
|
||||||
// Verify hash format: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
|
|
||||||
assert.Contains(t, hash, "$argon2id$")
|
|
||||||
assert.Contains(t, hash, "v=19")
|
|
||||||
assert.Contains(t, hash, "m=65536")
|
|
||||||
assert.Contains(t, hash, "t=3")
|
|
||||||
assert.Contains(t, hash, "p=4")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerify(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
password string
|
|
||||||
hash string
|
|
||||||
want bool
|
|
||||||
wantErr bool
|
|
||||||
skip bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "correct password",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "", // Will be generated
|
|
||||||
want: true,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "incorrect password",
|
|
||||||
password: "wrongpassword",
|
|
||||||
hash: "", // Will be generated from different password
|
|
||||||
want: false,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - too few parts",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "$argon2id$v=19$m=65536",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - wrong algorithm",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "$bcrypt$v=19$m=65536,t=3,p=4$salt$hash",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - malformed version",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "$argon2id$v=invalid$m=65536,t=3,p=4$salt$hash",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - malformed parameters",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "$argon2id$v=19$m=invalid,t=3,p=4$salt$hash",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - invalid base64 salt",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "$argon2id$v=19$m=65536,t=3,p=4$invalid-base64$hash",
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid hash format - invalid base64 hash",
|
|
||||||
password: "testpassword123",
|
|
||||||
hash: "", // Will be generated and then corrupted
|
|
||||||
want: false,
|
|
||||||
wantErr: true,
|
|
||||||
skip: true, // Skip this test - corrupting base64 is complex
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Skip test if marked
|
|
||||||
if tt.skip {
|
|
||||||
t.Skip("Skipping test")
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Generate hash if needed
|
|
||||||
if tt.hash == "" {
|
|
||||||
if tt.name == "incorrect password" {
|
|
||||||
// Generate hash for a different password
|
|
||||||
hash, err = Hash("differentpassword")
|
|
||||||
require.NoError(t, err)
|
|
||||||
} else {
|
|
||||||
hash, err = Hash(tt.password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hash = tt.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify
|
|
||||||
got, err := Verify(tt.password, hash)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.Error(t, err)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.want, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHash_Verify_RoundTrip(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
passwords := []string{
|
|
||||||
"testpassword123",
|
|
||||||
"",
|
|
||||||
"!@#$%^&*()",
|
|
||||||
"very long password with spaces and special characters !@#$%^&*()_+-=[]{}|;:,.<>?",
|
|
||||||
"unicode-测试-пароль",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, password := range passwords {
|
|
||||||
t.Run(password, func(t *testing.T) {
|
|
||||||
hash, err := Hash(password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotEmpty(t, hash)
|
|
||||||
|
|
||||||
// Verify the same password
|
|
||||||
valid, err := Verify(password, hash)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, valid, "Password should verify correctly")
|
|
||||||
|
|
||||||
// Verify different password
|
|
||||||
invalid, err := Verify("wrongpassword", hash)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, invalid, "Wrong password should not verify")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHash_Uniqueness(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
password := "testpassword123"
|
|
||||||
hashes := make(map[string]bool)
|
|
||||||
|
|
||||||
// Generate multiple hashes for the same password
|
|
||||||
// They should be different due to random salt
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
hash, err := Hash(password)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.NotContains(t, hashes, hash, "Each hash should be unique due to random salt")
|
|
||||||
hashes[hash] = true
|
|
||||||
|
|
||||||
// But all should verify correctly
|
|
||||||
valid, err := Verify(password, hash)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.True(t, valid, "All hashes should verify correctly")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user