mirror of
https://tvoygit.ru/Djam/abfmigrator.git
synced 2025-02-23 10:22:46 +00:00
upload sources
This commit is contained in:
parent
fc83c21c2e
commit
2c002e77ae
8 changed files with 1036 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.*~
|
||||||
|
*~
|
||||||
|
.env
|
||||||
|
cache.json
|
||||||
|
bin/*
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
// Используйте IntelliSense, чтобы узнать о возможных атрибутах.
|
||||||
|
// Наведите указатель мыши, чтобы просмотреть описания существующих атрибутов.
|
||||||
|
// Для получения дополнительной информации посетите: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/main.go"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
FROM docker.io/golang:1.23-alpine AS builder
|
||||||
|
|
||||||
|
ENV GOEXPORT=https://proxy.golang.org
|
||||||
|
|
||||||
|
COPY . /build
|
||||||
|
|
||||||
|
RUN cd /build && \
|
||||||
|
go get ./... && \
|
||||||
|
go build -ldflags '-s -w' -o abfmigrator && \
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
COPY --from=builder /build/abfmigrator .
|
||||||
|
|
||||||
|
ENTRYPOINT ["./abfmigrator"]
|
54
README.md
54
README.md
|
@ -3,3 +3,57 @@
|
||||||
Утилита миграции репозиториев, которая создает зеркала на в основанных на gitea репозиториях расположенных в abf
|
Утилита миграции репозиториев, которая создает зеркала на в основанных на gitea репозиториях расположенных в abf
|
||||||
(abf.io, abf.openmandriva.org)
|
(abf.io, abf.openmandriva.org)
|
||||||
|
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GOPROXY=direct
|
||||||
|
go get ./...
|
||||||
|
go build -o abfmigrator .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тесты
|
||||||
|
```bash
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
Создать в папке с программой файл `.env`
|
||||||
|
|
||||||
|
```
|
||||||
|
ABF_API_URL=https://abf.miproject.org
|
||||||
|
ABF_FILE_STORE_URL=https://abf.miproject.org
|
||||||
|
ABF_LOGIN=user
|
||||||
|
ABF_PASSWORD=password
|
||||||
|
GITEA_API_URL=https://mygitea.org/api/v1
|
||||||
|
GITEA_TOKEN=token_user_gitea
|
||||||
|
GITEA_OWNER=owner_gitea
|
||||||
|
PLATFORM_ID=abf_id_platform
|
||||||
|
REPO_ID=abf_id_repo
|
||||||
|
OWNER_NAME=abf_owner
|
||||||
|
```
|
||||||
|
|
||||||
|
где
|
||||||
|
1. `ABF_API_URL` - адрес ABF
|
||||||
|
2. `ABF_FILE_STORE_URL` - адрес файлового хранилища (не используется)
|
||||||
|
3. `ABF_LOGIN` - имя пользователя для входа в ABF (login)
|
||||||
|
4. `ABF_PASSWORD` - пароль пользователя ABF
|
||||||
|
5. `GITEA_API_URL` - адрес gitea
|
||||||
|
6. `GITEA_TOKEN` - токен пользователя gitea для доступа к API
|
||||||
|
7. `GITEA_OWNER` - владелец репозитория, часто пользователь или организация
|
||||||
|
8. `PLATFORM_ID` - ID платформы в ABF
|
||||||
|
9. `REPO_ID` - ID репозитория в ABF
|
||||||
|
10. `OWNER_NAME` - владелец платформы и репозитрия в ABF
|
||||||
|
|
||||||
|
## В docker
|
||||||
|
1. собрать контейнер
|
||||||
|
```
|
||||||
|
docker build --network=host -t abfmigrator .
|
||||||
|
```
|
||||||
|
2. запустить контейнер
|
||||||
|
```
|
||||||
|
docker run -v $PWD/.env:/.env abfmigrator
|
||||||
|
```
|
||||||
|
|
||||||
|
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module abfmigration
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
|
|
||||||
|
require github.com/joho/godotenv v1.5.1
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
798
main.go
Normal file
798
main.go
Normal file
|
@ -0,0 +1,798 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AbfJson структура для взаимодействия с API ABF.io
|
||||||
|
type AbfJson struct {
|
||||||
|
AbfURL string
|
||||||
|
FileStoreURL string
|
||||||
|
Login string
|
||||||
|
Password string
|
||||||
|
Base64AuthString string
|
||||||
|
Log *log.Logger
|
||||||
|
CacheData map[string][]byte
|
||||||
|
CacheEtags map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ошибки API ABF.io
|
||||||
|
var errorsMap = map[string]error{
|
||||||
|
"Invalid email or password.": fmt.Errorf("Authentication error"),
|
||||||
|
"403 Forbidden | Rate Limit Exceeded": fmt.Errorf("Rate limit exceeded"),
|
||||||
|
"Page not found": fmt.Errorf("Page not found"),
|
||||||
|
"Project has not been forked. Name has already been taken": fmt.Errorf("Name already taken"),
|
||||||
|
"Error 404. Resource not found!": fmt.Errorf("Resource not found"),
|
||||||
|
"Something went wrong. We've been notified about this issue and we'll take a look at it shortly.": fmt.Errorf("Internal server error"),
|
||||||
|
"We update the site, it will take some time. We are really trying to do it fast. We apologize for any inconvenience..": fmt.Errorf("Server under maintenance"),
|
||||||
|
"Requires authentication": fmt.Errorf("Authentication required"),
|
||||||
|
"Forbidden. Sorry, you don't have enough rights for this action!": fmt.Errorf("Forbidden"),
|
||||||
|
"Access violation to this page!": fmt.Errorf("Access violation"),
|
||||||
|
"Bad Request": fmt.Errorf("Bad request"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoodMessages сообщения, которые не считаются ошибками
|
||||||
|
var goodMessages = []string{
|
||||||
|
"Errors during build publishing!",
|
||||||
|
"Build is queued for publishing",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FatalErrors фатальные ошибки, которые приводят к выходу из программы
|
||||||
|
var fatalErrors = []error{
|
||||||
|
errorsMap["Invalid email or password."],
|
||||||
|
errorsMap["403 Forbidden | Rate Limit Exceeded"],
|
||||||
|
errorsMap["Something went wrong. We've been notified about this issue and we'll take a look at it shortly."],
|
||||||
|
}
|
||||||
|
|
||||||
|
// Project структура для хранения информации о проекте
|
||||||
|
type Project struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectDetails структура для хранения деталей проекта
|
||||||
|
type ProjectDetails struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"git_url"`
|
||||||
|
WebURL string `json:"web_url"`
|
||||||
|
SSHURLToRepo string `json:"ssh_url_to_repo"`
|
||||||
|
HTTPURLToRepo string `json:"http_url_to_repo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheEntry структура для хранения данных кеша
|
||||||
|
type CacheEntry struct {
|
||||||
|
Data map[string][]byte `json:"data"`
|
||||||
|
CacheEtags map[string]string `json:"cache_etags"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAbfJson создает новый экземпляр AbfJson
|
||||||
|
func NewAbfJson(abfURL, fileStoreURL, login, password string, log *log.Logger) (*AbfJson, error) {
|
||||||
|
if !strings.HasPrefix(fileStoreURL, "http://") && !strings.HasPrefix(fileStoreURL, "https://") {
|
||||||
|
log.Fatalf("File-store URL has to start with 'http(s)://'")
|
||||||
|
}
|
||||||
|
|
||||||
|
lpw := fmt.Sprintf("%s:%s", login, password)
|
||||||
|
encodedLpw := base64.StdEncoding.EncodeToString([]byte(lpw))
|
||||||
|
|
||||||
|
return &AbfJson{
|
||||||
|
AbfURL: strings.TrimSuffix(abfURL, "/"),
|
||||||
|
FileStoreURL: strings.TrimSuffix(fileStoreURL, "/"),
|
||||||
|
Login: login,
|
||||||
|
Password: password,
|
||||||
|
Base64AuthString: encodedLpw,
|
||||||
|
Log: log,
|
||||||
|
CacheData: make(map[string][]byte),
|
||||||
|
CacheEtags: make(map[string]string),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCache загружает кеш из файла
|
||||||
|
func (a *AbfJson) LoadCache(cacheFilePath string) error {
|
||||||
|
data, err := ioutil.ReadFile(cacheFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
a.Log.Println("Cache file does not exist. Proceeding without cache.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read cache file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheEntry CacheEntry
|
||||||
|
if err := json.Unmarshal(data, &cacheEntry); err != nil {
|
||||||
|
return fmt.Errorf("failed to unmarshal cache data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.CacheData = cacheEntry.Data
|
||||||
|
a.CacheEtags = cacheEntry.CacheEtags
|
||||||
|
a.Log.Println("Cache loaded successfully.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveCache сохраняет кеш в файл
|
||||||
|
func (a *AbfJson) SaveCache(cacheFilePath string) error {
|
||||||
|
cacheEntry := CacheEntry{
|
||||||
|
Data: a.CacheData,
|
||||||
|
CacheEtags: a.CacheEtags,
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cacheEntry)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal cache data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(cacheFilePath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write cache file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Log.Println("Cache saved successfully.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCacheValid проверяет, действителен ли кеш
|
||||||
|
func (a *AbfJson) IsCacheValid(cacheFilePath string, ttl time.Duration) (bool, error) {
|
||||||
|
data, err := ioutil.ReadFile(cacheFilePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
a.Log.Println("Cache file does not exist. Cache is invalid.")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("failed to read cache file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheEntry CacheEntry
|
||||||
|
if err := json.Unmarshal(data, &cacheEntry); err != nil {
|
||||||
|
return false, fmt.Errorf("failed to unmarshal cache data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(cacheEntry.Timestamp) > ttl {
|
||||||
|
a.Log.Println("Cache has expired.")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Log.Println("Cache is valid.")
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessResponse обрабатывает ответ API ABF.io
|
||||||
|
func (a *AbfJson) ProcessResponse(responseString []byte) (map[string]interface{}, error) {
|
||||||
|
var res map[string]interface{}
|
||||||
|
if err := json.Unmarshal(responseString, &res); err != nil {
|
||||||
|
a.Log.Fatalf("Internal server error: it has returned non-json data.")
|
||||||
|
}
|
||||||
|
|
||||||
|
message, ok := res["message"].(string)
|
||||||
|
if ok && !contains(goodMessages, message) {
|
||||||
|
return nil, errorsMap[message]
|
||||||
|
}
|
||||||
|
|
||||||
|
repository, repoOk := res["repository"].(map[string]interface{})
|
||||||
|
project, projOk := res["project"].(map[string]interface{})
|
||||||
|
|
||||||
|
if repoOk {
|
||||||
|
repoMessage, ok := repository["message"].(string)
|
||||||
|
if ok && (strings.Contains(repoMessage, "error") || strings.Contains(repoMessage, "has not been")) {
|
||||||
|
return nil, errorsMap[repoMessage]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if projOk {
|
||||||
|
projMessage, ok := project["message"].(string)
|
||||||
|
if ok && (strings.Contains(projMessage, "error") || strings.Contains(projMessage, "has not been")) {
|
||||||
|
return nil, errorsMap[projMessage]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errStr, ok := res["error"].(string); ok && !contains(goodMessages, errStr) {
|
||||||
|
return nil, errorsMap[errStr]
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURLContents выполняет HTTP-запрос к API ABF.io
|
||||||
|
func (a *AbfJson) GetURLContents(path string, params map[string]string, method string, body interface{}) (interface{}, error) {
|
||||||
|
url := a.AbfURL + path
|
||||||
|
if method == "file_store" {
|
||||||
|
url = a.FileStoreURL + path
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
query := ""
|
||||||
|
for key, value := range params {
|
||||||
|
if query == "" {
|
||||||
|
query += "?"
|
||||||
|
} else {
|
||||||
|
query += "&"
|
||||||
|
}
|
||||||
|
query += fmt.Sprintf("%s=%s", key, value)
|
||||||
|
}
|
||||||
|
url += query
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Log.Printf("Fetching URL: %s", url)
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
var requestBody []byte
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
requestBody, err = json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Fatalf("Failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
var prettyJSON bytes.Buffer
|
||||||
|
json.Indent(&prettyJSON, requestBody, "", " ")
|
||||||
|
a.Log.Printf("Request Body: %s", prettyJSON.String()) // Логирование отправляемого JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case "POST":
|
||||||
|
req, err = http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
|
||||||
|
case "PUT":
|
||||||
|
req, err = http.NewRequest("PUT", url, bytes.NewBuffer(requestBody))
|
||||||
|
case "DELETE":
|
||||||
|
req, err = http.NewRequest("DELETE", url, bytes.NewBuffer(requestBody))
|
||||||
|
default:
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Fatalf("Failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", a.Base64AuthString))
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
etag, etagFound := a.CacheEtags[url]
|
||||||
|
if etagFound {
|
||||||
|
req.Header.Add("If-None-Match", etag)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Fatalf("HTTP request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Fatalf("Failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == 304 && etagFound {
|
||||||
|
a.Log.Println("Getting cached result (cache was validated)")
|
||||||
|
return a.ProcessResponse(a.CacheData[etag])
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 201 {
|
||||||
|
a.Log.Printf("Return code: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(responseBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
etagNew := resp.Header.Get("ETag")
|
||||||
|
if etagNew != "" {
|
||||||
|
a.Log.Printf("Caching the new value for %s. ETag is %s", url, etagNew)
|
||||||
|
a.CacheEtags[url] = etagNew
|
||||||
|
a.CacheData[etagNew] = responseBody
|
||||||
|
}
|
||||||
|
|
||||||
|
var res interface{}
|
||||||
|
if err := json.Unmarshal(responseBody, &res); err != nil {
|
||||||
|
a.Log.Fatalf("Failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectsList получает список проектов с обходом всех страниц
|
||||||
|
func (a *AbfJson) GetProjectsList(platformID, repoID int, ownerName string) ([]Project, error) {
|
||||||
|
var allProjects []Project
|
||||||
|
page := 1
|
||||||
|
perPage := 100
|
||||||
|
|
||||||
|
for {
|
||||||
|
params := map[string]string{
|
||||||
|
"added": "true",
|
||||||
|
"format": "json",
|
||||||
|
"owner_name": ownerName,
|
||||||
|
"page": fmt.Sprintf("%d", page),
|
||||||
|
"per_page": fmt.Sprintf("%d", perPage),
|
||||||
|
"project_name": "",
|
||||||
|
}
|
||||||
|
path := fmt.Sprintf("/platforms/%d/repositories/%d/projects_list", platformID, repoID)
|
||||||
|
res, err := a.GetURLContents(path, params, "GET", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to get projects list: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем res["projects"] в []byte
|
||||||
|
projectsArray, ok := res.(map[string]interface{})["projects"]
|
||||||
|
if !ok {
|
||||||
|
a.Log.Fatalf("Failed to find 'projects' in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectsBytes, err := json.Marshal(projectsArray)
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Fatalf("Failed to marshal projects list: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var projects []Project
|
||||||
|
if err := json.Unmarshal(projectsBytes, &projects); err != nil {
|
||||||
|
a.Log.Fatalf("Failed to unmarshal projects list: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allProjects = append(allProjects, projects...)
|
||||||
|
|
||||||
|
if len(projects) < perPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
return allProjects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectDetails получает детали проекта по ID
|
||||||
|
func (a *AbfJson) GetProjectDetails(projectID int) (*ProjectDetails, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d.json", projectID)
|
||||||
|
res, err := a.GetURLContents(path, nil, "GET", nil)
|
||||||
|
if err != nil {
|
||||||
|
a.Log.Printf("Failed to get project details for project ID %d: %v", projectID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectData, ok := res.(map[string]interface{})["project"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
a.Log.Printf("Missing 'project' field in response for project ID %d", projectID)
|
||||||
|
return nil, fmt.Errorf("Missing 'project' field in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
id, idOk := projectData["id"]
|
||||||
|
if !idOk {
|
||||||
|
a.Log.Printf("Missing 'id' field in project details for project ID %d", projectID)
|
||||||
|
return nil, fmt.Errorf("Missing 'id' field in project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
idFloat, idOk := id.(float64)
|
||||||
|
if !idOk {
|
||||||
|
a.Log.Printf("Invalid 'id' type in project details for project ID %d: expected float64, got %T", projectID, id)
|
||||||
|
return nil, fmt.Errorf("Invalid 'id' type in project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
name, nameOk := projectData["name"].(string)
|
||||||
|
if !nameOk {
|
||||||
|
a.Log.Printf("Missing 'name' field in project details for project ID %d", projectID)
|
||||||
|
return nil, fmt.Errorf("Missing 'name' field in project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
gitUrl, gitUrlOk := projectData["git_url"].(string)
|
||||||
|
webUrl, webUrlOk := projectData["web_url"].(string)
|
||||||
|
sshUrlToRepo, sshUrlToRepoOk := projectData["ssh_url_to_repo"].(string)
|
||||||
|
httpUrlToRepo, httpUrlToRepoOk := projectData["http_url_to_repo"].(string)
|
||||||
|
|
||||||
|
var url string
|
||||||
|
if gitUrlOk {
|
||||||
|
url = gitUrl
|
||||||
|
} else if httpUrlToRepoOk {
|
||||||
|
url = httpUrlToRepo
|
||||||
|
} else if webUrlOk {
|
||||||
|
url = webUrl
|
||||||
|
} else if sshUrlToRepoOk {
|
||||||
|
url = sshUrlToRepo
|
||||||
|
} else {
|
||||||
|
a.Log.Printf("Missing URL fields in project details for project ID %d", projectID)
|
||||||
|
return nil, fmt.Errorf("Missing URL fields in project details")
|
||||||
|
}
|
||||||
|
|
||||||
|
projectDetails := &ProjectDetails{
|
||||||
|
ID: int(idFloat),
|
||||||
|
Name: name,
|
||||||
|
URL: url,
|
||||||
|
WebURL: webUrl,
|
||||||
|
SSHURLToRepo: sshUrlToRepo,
|
||||||
|
HTTPURLToRepo: httpUrlToRepo,
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains проверяет, содержит ли слайс строк строку
|
||||||
|
func contains(slice []string, str string) bool {
|
||||||
|
for _, item := range slice {
|
||||||
|
if item == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GiteaClient структура для взаимодействия с API Gitea
|
||||||
|
type GiteaClient struct {
|
||||||
|
APIURL string
|
||||||
|
Token string
|
||||||
|
Owner string
|
||||||
|
Log *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository структура для хранения информации о репозитории в Gitea
|
||||||
|
type Repository struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepositories получает список репозиториев из Gitea
|
||||||
|
func (g *GiteaClient) GetRepositories() ([]Repository, error) {
|
||||||
|
path := "/user/repos"
|
||||||
|
res, err := g.GetURLContents(path, nil, "GET", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to get repositories: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что res является массивом
|
||||||
|
reposArray, ok := res.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
g.Log.Fatalf("Expected array in response, got %T", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
var repos []Repository
|
||||||
|
for _, repoInterface := range reposArray {
|
||||||
|
repoMap, ok := repoInterface.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
g.Log.Fatalf("Expected map in array element, got %T", repoInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, idOk := repoMap["id"].(float64)
|
||||||
|
if !idOk {
|
||||||
|
g.Log.Fatalf("Invalid 'id' type in repository: expected float64, got %T", repoMap["id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
name, nameOk := repoMap["name"].(string)
|
||||||
|
if !nameOk {
|
||||||
|
g.Log.Fatalf("Missing 'name' field in repository")
|
||||||
|
}
|
||||||
|
|
||||||
|
repos = append(repos, Repository{
|
||||||
|
ID: int(id),
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return repos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorRepositoryPayload структура для запроса зеркалирования репозитория в Gitea
|
||||||
|
type MirrorRepositoryPayload struct {
|
||||||
|
AuthPassword string `json:"auth_password,omitempty"`
|
||||||
|
AuthToken string `json:"auth_token,omitempty"`
|
||||||
|
AuthUsername string `json:"auth_username,omitempty"`
|
||||||
|
CloneAddr string `json:"clone_addr"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Issues bool `json:"issues"`
|
||||||
|
Labels bool `json:"labels"`
|
||||||
|
LFS bool `json:"lfs"`
|
||||||
|
LFSEndpoint string `json:"lfs_endpoint,omitempty"`
|
||||||
|
Milestones bool `json:"milestones"`
|
||||||
|
Mirror bool `json:"mirror"`
|
||||||
|
MirrorInterval string `json:"mirror_interval,omitempty"`
|
||||||
|
Private bool `json:"private"`
|
||||||
|
PullRequests bool `json:"pull_requests"`
|
||||||
|
Releases bool `json:"releases"`
|
||||||
|
RepoName string `json:"repo_name"`
|
||||||
|
RepoOwner string `json:"repo_owner"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
UID int `json:"uid,omitempty"`
|
||||||
|
Wiki bool `json:"wiki"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorRepository создает зеркало репозитория в Gitea
|
||||||
|
func (g *GiteaClient) MirrorRepository(repoName, cloneURL string) error {
|
||||||
|
payload := MirrorRepositoryPayload{
|
||||||
|
CloneAddr: cloneURL,
|
||||||
|
Description: "",
|
||||||
|
Issues: true,
|
||||||
|
Labels: true,
|
||||||
|
LFS: false,
|
||||||
|
Milestones: true,
|
||||||
|
Mirror: true,
|
||||||
|
MirrorInterval: "8h0m0s", // Интервал зеркалирования
|
||||||
|
Private: false,
|
||||||
|
PullRequests: true,
|
||||||
|
Releases: true,
|
||||||
|
RepoName: repoName,
|
||||||
|
RepoOwner: g.Owner,
|
||||||
|
Service: "git",
|
||||||
|
Wiki: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
path := "/repos/migrate"
|
||||||
|
res, err := g.GetURLContents(path, nil, "POST", payload)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to mirror repository: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res == nil {
|
||||||
|
return fmt.Errorf("Empty response from Gitea")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log.Printf("Response from Gitea: %+v", res)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetURLContents выполняет HTTP-запрос к API Gitea
|
||||||
|
func (g *GiteaClient) GetURLContents(path string, params map[string]string, method string, body interface{}) (interface{}, error) {
|
||||||
|
url := g.APIURL + path
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
query := ""
|
||||||
|
for key, value := range params {
|
||||||
|
if query == "" {
|
||||||
|
query += "?"
|
||||||
|
} else {
|
||||||
|
query += "&"
|
||||||
|
}
|
||||||
|
query += fmt.Sprintf("%s=%s", key, value)
|
||||||
|
}
|
||||||
|
url += query
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Log.Printf("Fetching URL: %s", url)
|
||||||
|
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
var requestBody []byte
|
||||||
|
|
||||||
|
if body != nil {
|
||||||
|
requestBody, err = json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
g.Log.Fatalf("Failed to marshal request body: %v", err)
|
||||||
|
}
|
||||||
|
var prettyJSON bytes.Buffer
|
||||||
|
json.Indent(&prettyJSON, requestBody, "", " ")
|
||||||
|
g.Log.Printf("Request Body: %s", prettyJSON.String()) // Логирование отправляемого JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case "POST":
|
||||||
|
req, err = http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
|
||||||
|
case "PUT":
|
||||||
|
req, err = http.NewRequest("PUT", url, bytes.NewBuffer(requestBody))
|
||||||
|
case "DELETE":
|
||||||
|
req, err = http.NewRequest("DELETE", url, bytes.NewBuffer(requestBody))
|
||||||
|
default:
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
g.Log.Fatalf("Failed to create request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("token %s", g.Token))
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
g.Log.Fatalf("HTTP request failed: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
g.Log.Fatalf("Failed to read response body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 && resp.StatusCode != 201 {
|
||||||
|
g.Log.Printf("Return code: %d", resp.StatusCode)
|
||||||
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(responseBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var res interface{}
|
||||||
|
if err := json.Unmarshal(responseBody, &res); err != nil {
|
||||||
|
g.Log.Fatalf("Failed to unmarshal response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Флаги для параметров проектов
|
||||||
|
platformIDFlag := flag.Int("platformid", 0, "Platform ID")
|
||||||
|
repoIDFlag := flag.Int("repoid", 0, "Repository ID")
|
||||||
|
ownerFlag := flag.String("owner", "", "Owner name")
|
||||||
|
cacheTTLFlag := flag.Duration("cachettl", 12*time.Hour, "Cache TTL duration")
|
||||||
|
forceFlag := flag.Bool("force", false, "Force refresh cache")
|
||||||
|
cacheFilePathFlag := flag.String("cachefile", "cache.json", "Cache file path")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Загрузка переменных окружения из файла .env
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error loading .env file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
abfURL := os.Getenv("ABF_API_URL")
|
||||||
|
fileStoreURL := os.Getenv("ABF_FILE_STORE_URL")
|
||||||
|
login := os.Getenv("ABF_LOGIN")
|
||||||
|
password := os.Getenv("ABF_PASSWORD")
|
||||||
|
giteaAPIURL := os.Getenv("GITEA_API_URL")
|
||||||
|
giteaToken := os.Getenv("GITEA_TOKEN")
|
||||||
|
giteaOwner := os.Getenv("GITEA_OWNER")
|
||||||
|
cacheFilePath := *cacheFilePathFlag
|
||||||
|
|
||||||
|
// Приоритет флагов над переменными окружения
|
||||||
|
platformID := *platformIDFlag
|
||||||
|
if platformID == 0 {
|
||||||
|
platformIDStr := os.Getenv("PLATFORM_ID")
|
||||||
|
if platformIDStr == "" {
|
||||||
|
log.Fatalf("Platform ID is not set either via --platformid flag or PLATFORM_ID environment variable")
|
||||||
|
}
|
||||||
|
platformID, err = strconv.Atoi(platformIDStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Invalid PLATFORM_ID value: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repoID := *repoIDFlag
|
||||||
|
if repoID == 0 {
|
||||||
|
repoIDStr := os.Getenv("REPO_ID")
|
||||||
|
if repoIDStr == "" {
|
||||||
|
log.Fatalf("Repository ID is not set either via --repoid flag or REPO_ID environment variable")
|
||||||
|
}
|
||||||
|
repoID, err = strconv.Atoi(repoIDStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Invalid REPO_ID value: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerName := *ownerFlag
|
||||||
|
if ownerName == "" {
|
||||||
|
ownerName = os.Getenv("OWNER_NAME")
|
||||||
|
if ownerName == "" {
|
||||||
|
log.Fatalf("Owner name is not set either via --owner flag or OWNER_NAME environment variable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.New(os.Stdout, "", log.LstdFlags)
|
||||||
|
|
||||||
|
abfClient, err := NewAbfJson(abfURL, fileStoreURL, login, password, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Failed to create ABF client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка кеша
|
||||||
|
cacheValid, err := abfClient.IsCacheValid(cacheFilePath, *cacheTTLFlag)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Failed to check cache validity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cacheValid && !*forceFlag {
|
||||||
|
logger.Println("Using cached data.")
|
||||||
|
} else {
|
||||||
|
logger.Println("Refreshing cache.")
|
||||||
|
|
||||||
|
// Получение списка проектов
|
||||||
|
projects, err := abfClient.GetProjectsList(platformID, repoID, ownerName)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Failed to get projects: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Printf("Total projects found: %d", len(projects))
|
||||||
|
|
||||||
|
// Сбор ID и имени проектов
|
||||||
|
projectIDs := make(map[int]string)
|
||||||
|
for _, project := range projects {
|
||||||
|
projectIDs[project.ID] = project.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение деталей проектов и их URL
|
||||||
|
var projectDetailsList []*ProjectDetails
|
||||||
|
for projectID, projectName := range projectIDs {
|
||||||
|
details, err := abfClient.GetProjectDetails(projectID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Failed to get details for project %d (%s): %v", projectID, projectName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
projectDetailsList = append(projectDetailsList, details)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранение кеша
|
||||||
|
abfClient.CacheData = make(map[string][]byte)
|
||||||
|
for _, details := range projectDetailsList {
|
||||||
|
detailsBytes, err := json.Marshal(details)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Failed to marshal project details: %v", err)
|
||||||
|
}
|
||||||
|
abfClient.CacheData[strconv.Itoa(details.ID)] = detailsBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := abfClient.SaveCache(cacheFilePath); err != nil {
|
||||||
|
logger.Fatalf("Failed to save cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вывод списка проектов с URL
|
||||||
|
for _, details := range projectDetailsList {
|
||||||
|
logger.Printf("Project ID: %d, Name: %s, URL: %s", details.ID, details.Name, details.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загрузка кеша для дальнейшей работы
|
||||||
|
if err := abfClient.LoadCache(cacheFilePath); err != nil {
|
||||||
|
logger.Fatalf("Failed to load cache: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение списка репозиториев из Gitea
|
||||||
|
giteaClient := &GiteaClient{
|
||||||
|
APIURL: giteaAPIURL,
|
||||||
|
Token: giteaToken,
|
||||||
|
Owner: giteaOwner,
|
||||||
|
Log: logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaRepos, err := giteaClient.GetRepositories()
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("Failed to get repositories from Gitea: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaRepoNames := make(map[string]struct{})
|
||||||
|
for _, repo := range giteaRepos {
|
||||||
|
giteaRepoNames[repo.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Миграция репозиториев в Gitea
|
||||||
|
for projectIDStr, detailsBytes := range abfClient.CacheData {
|
||||||
|
var projectDetails ProjectDetails
|
||||||
|
if err := json.Unmarshal(detailsBytes, &projectDetails); err != nil {
|
||||||
|
logger.Printf("Failed to unmarshal project details: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID, err := strconv.Atoi(projectIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Недопустимый идентификатор проекта %s: %v", projectIDStr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используйте переменную projectID здесь
|
||||||
|
logger.Printf("Идентификатор проекта: %d", projectID)
|
||||||
|
|
||||||
|
repoName := projectDetails.Name
|
||||||
|
cloneURL := projectDetails.URL
|
||||||
|
|
||||||
|
if _, exists := giteaRepoNames[repoName]; exists {
|
||||||
|
logger.Printf("Repository %s already exists in Gitea. Skipping...", repoName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = giteaClient.MirrorRepository(repoName, cloneURL)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("Failed to mirror repository %s: %v", repoName, err)
|
||||||
|
} else {
|
||||||
|
logger.Printf("Repository %s mirrored to Gitea.", repoName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
140
main_test.go
Normal file
140
main_test.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Тестирование метода MirrorRepository с неверным телом запроса
|
||||||
|
func TestMirrorRepositoryInvalidPayload(t *testing.T) {
|
||||||
|
// Создаем мок сервера
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != "POST" {
|
||||||
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if contentType != "application/json" {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := req.Header.Get("Authorization")
|
||||||
|
if auth != "token test-token" {
|
||||||
|
rw.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload MirrorRepositoryPayload
|
||||||
|
err := json.NewDecoder(req.Body).Decode(&payload)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем корректность полей payload
|
||||||
|
if payload.CloneAddr != "https://djam@abf.io/djam/btunlock.git" ||
|
||||||
|
payload.RepoName != "btunlock" ||
|
||||||
|
payload.RepoOwner != "djam" ||
|
||||||
|
!payload.Issues ||
|
||||||
|
!payload.Labels ||
|
||||||
|
payload.LFS ||
|
||||||
|
!payload.Milestones ||
|
||||||
|
!payload.Mirror ||
|
||||||
|
payload.MirrorInterval != "8h0m0s" ||
|
||||||
|
payload.Private ||
|
||||||
|
!payload.PullRequests ||
|
||||||
|
!payload.Releases ||
|
||||||
|
payload.Service != "git" ||
|
||||||
|
!payload.Wiki {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Set("Content-Type", "application/json")
|
||||||
|
rw.WriteHeader(http.StatusCreated)
|
||||||
|
rw.Write([]byte(`{"id": 123, "name": "btunlock", "full_name": "djam/btunlock"}`))
|
||||||
|
}))
|
||||||
|
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Создаем клиент для тестирования
|
||||||
|
giteaClient := &GiteaClient{
|
||||||
|
APIURL: server.URL,
|
||||||
|
Token: "test-token",
|
||||||
|
Owner: "djam",
|
||||||
|
Log: log.New(os.Stdout, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем метод MirrorRepository с неверным телом запроса
|
||||||
|
err := giteaClient.MirrorRepository("btunlock", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что ошибка соответствует ожидаемому сообщению
|
||||||
|
expectedErrorMessage := "Failed to mirror repository: HTTP error 400: "
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Errorf("Expected error message %q, got %q", expectedErrorMessage, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тестирование метода MirrorRepository с ошибкой 422
|
||||||
|
func TestMirrorRepositoryUnprocessableEntity(t *testing.T) {
|
||||||
|
// Создаем мок сервера
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != "POST" {
|
||||||
|
rw.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if contentType != "application/json" {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := req.Header.Get("Authorization")
|
||||||
|
if auth != "token test-token" {
|
||||||
|
rw.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload MirrorRepositoryPayload
|
||||||
|
err := json.NewDecoder(req.Body).Decode(&payload)
|
||||||
|
if err != nil {
|
||||||
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
}))
|
||||||
|
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Создаем клиент для тестирования
|
||||||
|
giteaClient := &GiteaClient{
|
||||||
|
APIURL: server.URL,
|
||||||
|
Token: "test-token",
|
||||||
|
Owner: "djam",
|
||||||
|
Log: log.New(os.Stdout, "", log.LstdFlags),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вызываем метод MirrorRepository с ошибкой 422
|
||||||
|
err := giteaClient.MirrorRepository("btunlock", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что ошибка соответствует ожидаемому сообщению
|
||||||
|
expectedErrorMessage := "Failed to mirror repository: HTTP error 422: "
|
||||||
|
if err.Error() != expectedErrorMessage {
|
||||||
|
t.Errorf("Expected error message %q, got %q", expectedErrorMessage, err.Error())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue