Skip to content
This repository was archived by the owner on Oct 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
pull_request:
push:
branches: [master]

jobs:
verify:
name: Vet, test and build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Check formatting
# gofmt -l prints the files that need formatting; anything printed fails.
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "These files are not gofmt'd:"
echo "$unformatted"
exit 1
fi

- run: go vet ./...

# -race because the cache and the HTTP handlers run concurrently, and a
# data race there would surface as a wrong validation result under load.
- run: go test ./... -race -coverprofile=coverage.out

- run: go build ./...

- name: Coverage summary
run: go tool cover -func=coverage.out | tail -1
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ docker-compose.override.yml

Thumbs.db

vcs.xml
vcs.xml
# JetBrains project files are per-developer, not per-project.
.idea/
*.iml
8 changes: 0 additions & 8 deletions .idea/.gitignore

This file was deleted.

9 changes: 0 additions & 9 deletions .idea/api-data-validator.iml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/git_toolbox_blame.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Jeferson Siqueira

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
21 changes: 21 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ const docTemplate = `{
"name": "cpf",
"in": "query"
},
{
"type": "string",
"description": "CNPJ to be validated",
"name": "cnpj",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand All @@ -47,6 +53,12 @@ const docTemplate = `{
"name": "telephone",
"in": "query"
},
{
"type": "string",
"description": "Phone number to be validated",
"name": "phone",
"in": "query"
},
{
"type": "string",
"description": "Credit card number to be validated",
Expand Down Expand Up @@ -78,6 +90,12 @@ const docTemplate = `{
"schema": {
"$ref": "#/definitions/models.ValidationResponse"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/models.ValidationResponse"
}
}
}
}
Expand All @@ -87,6 +105,9 @@ const docTemplate = `{
"models.ValidationResponse": {
"type": "object",
"properties": {
"error_code": {
"type": "string"
},
"execution_time_ms": {
"type": "integer"
},
Expand Down
21 changes: 21 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@
"name": "cpf",
"in": "query"
},
{
"type": "string",
"description": "CNPJ to be validated",
"name": "cnpj",
"in": "query"
},
{
"type": "string",
"description": "Name to be validated",
Expand All @@ -36,6 +42,12 @@
"name": "telephone",
"in": "query"
},
{
"type": "string",
"description": "Phone number to be validated",
"name": "phone",
"in": "query"
},
{
"type": "string",
"description": "Credit card number to be validated",
Expand Down Expand Up @@ -67,6 +79,12 @@
"schema": {
"$ref": "#/definitions/models.ValidationResponse"
}
},
"422": {
"description": "Unprocessable Entity",
"schema": {
"$ref": "#/definitions/models.ValidationResponse"
}
}
}
}
Expand All @@ -76,6 +94,9 @@
"models.ValidationResponse": {
"type": "object",
"properties": {
"error_code": {
"type": "string"
},
"execution_time_ms": {
"type": "integer"
},
Expand Down
14 changes: 14 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
definitions:
models.ValidationResponse:
properties:
error_code:
type: string
execution_time_ms:
type: integer
from_cache:
Expand Down Expand Up @@ -41,6 +43,10 @@ paths:
in: query
name: cpf
type: string
- description: CNPJ to be validated
in: query
name: cnpj
type: string
- description: Name to be validated
in: query
name: name
Expand All @@ -49,6 +55,10 @@ paths:
in: query
name: telephone
type: string
- description: Phone number to be validated
in: query
name: phone
type: string
- description: Credit card number to be validated
in: query
name: plastic
Expand All @@ -70,6 +80,10 @@ paths:
description: Bad Request
schema:
$ref: '#/definitions/models.ValidationResponse'
"422":
description: Unprocessable Entity
schema:
$ref: '#/definitions/models.ValidationResponse'
summary: Validates provided data format
tags:
- Validation
Expand Down
7 changes: 7 additions & 0 deletions handlers/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
// @Tags Validation
// @Param email query string false "Email to be validated"
// @Param cpf query string false "CPF to be validated"
// @Param cnpj query string false "CNPJ to be validated"
// @Param name query string false "Name to be validated"
// @Param telephone query string false "Phone number to be validated"
// @Param phone query string false "Phone number to be validated"
Expand Down Expand Up @@ -51,6 +52,9 @@ func ValidateHandler(w http.ResponseWriter, r *http.Request) {
} else if cpf := r.URL.Query().Get("cpf"); cpf != "" {
isValid, sanitizedValue, message, fromCache := utils.ValidateCPFWithCache(cpf)
response = createResponse("cpf", cpf, sanitizedValue, isValid, message, start, fromCache)
} else if cnpj := r.URL.Query().Get("cnpj"); cnpj != "" {
isValid, sanitizedValue, message := utils.ValidateCNPJ(cnpj)
response = createResponse("cnpj", cnpj, sanitizedValue, isValid, message, start, false)
} else if name := r.URL.Query().Get("name"); name != "" {
isValid, sanitizedValue, message := utils.ValidateName(name)
response = createResponse("name", name, sanitizedValue, isValid, message, start, false)
Expand Down Expand Up @@ -87,6 +91,9 @@ func validationCount(r *http.Request) int {
if strings.TrimSpace(r.URL.Query().Get("email")) != "" {
count++
}
if strings.TrimSpace(r.URL.Query().Get("cnpj")) != "" {
count++
}
if strings.TrimSpace(r.URL.Query().Get("cpf")) != "" {
count++
}
Expand Down
34 changes: 34 additions & 0 deletions handlers/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,37 @@ func TestValidateHandlerCPFValidationFailureStatus(t *testing.T) {
t.Fatalf("expected VALIDATION_FAILED, got %s", response.ErrorCode)
}
}

func TestValidateHandlerCNPJ(t *testing.T) {
tests := []struct {
name string
query string
wantStatus int
wantValid bool
}{
{"valid_cnpj", "cnpj=33.000.167/0001-01", http.StatusOK, true},
{"invalid_cnpj", "cnpj=33.000.167/0001-11", http.StatusUnprocessableEntity, false},
// CNPJ has to take part in the one-parameter-per-request rule, or it
// becomes the hole through which two validations arrive at once.
{"cnpj_with_cpf", "cnpj=33.000.167/0001-01&cpf=529.982.247-25", http.StatusBadRequest, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
ValidateHandler(recorder, httptest.NewRequest(http.MethodGet, "/validate?"+tt.query, nil))

if recorder.Code != tt.wantStatus {
t.Fatalf("expected status %d, got %d: %s", tt.wantStatus, recorder.Code, recorder.Body.String())
}

var response models.ValidationResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatalf("could not decode the response: %v", err)
}
if response.IsValid != tt.wantValid {
t.Fatalf("expected is_valid=%v, got %v (%s)", tt.wantValid, response.IsValid, response.Message)
}
})
}
}
42 changes: 42 additions & 0 deletions utils/cnpj_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package utils

import "testing"

func TestValidateCNPJ(t *testing.T) {
tests := []struct {
name string
cnpj string
valid bool
sanitized string
}{
// Petrobras and the Banco do Brasil — real, published CNPJs.
{"formatted", "33.000.167/0001-01", true, "33000167000101"},
{"bare", "00000000000191", true, "00000000000191"},
{"spaced", "33 000 167 0001 01", true, "33000167000101"},

{"wrong_first_check_digit", "33.000.167/0001-11", false, "33000167000111"},
{"wrong_second_check_digit", "33.000.167/0001-02", false, "33000167000102"},
{"too_short", "33.000.167/0001", false, "330001670001"},
{"too_long", "33.000.167/0001-010", false, "330001670001010"},

// Passes the check-digit arithmetic, so it needs excluding explicitly.
{"all_zeros", "00.000.000/0000-00", false, "00000000000000"},
{"all_ones", "11.111.111/1111-11", false, "11111111111111"},

// The guard from the sanitisation fix must cover CNPJ too.
{"trailing_letters", "33.000.167/0001-01abc", false, "33.000.167/0001-01abc"},
{"leading_letters", "abc33.000.167/0001-01", false, "abc33.000.167/0001-01"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid, sanitized, message := ValidateCNPJ(tt.cnpj)
if isValid != tt.valid {
t.Fatalf("expected valid=%v, got %v (%s)", tt.valid, isValid, message)
}
if sanitized != tt.sanitized {
t.Fatalf("expected sanitized %q, got %q", tt.sanitized, sanitized)
}
})
}
}
Loading
Loading