mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 07:02:09 +01:00
* Add comprehensive Docker support with multi-stage builds * Set up GitHub Container Registry integration * Enhance CI/CD workflows with Docker build and push capabilities * Add --help and --version flags to main application * Update documentation with Docker usage examples * Implement security best practices for container deployment
488 lines
12 KiB
Go
488 lines
12 KiB
Go
// Package main_test provides tests for the main package utility functions.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestIsURI tests the isURI function with various input scenarios.
|
|
func TestIsURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "valid HTTP URI",
|
|
input: "http://example.com",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "valid HTTPS URI",
|
|
input: "https://example.com",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "valid Articulate Rise URI",
|
|
input: "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "local file path",
|
|
input: "C:\\Users\\test\\file.json",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "relative file path",
|
|
input: "./sample.json",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "filename only",
|
|
input: "sample.json",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "short string",
|
|
input: "http",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "malformed URI",
|
|
input: "htp://example.com",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "FTP URI",
|
|
input: "ftp://example.com",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "HTTP with extra characters",
|
|
input: "xhttp://example.com",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isURI(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("isURI(%q) = %v, want %v", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkIsURI benchmarks the isURI function performance.
|
|
func BenchmarkIsURI(b *testing.B) {
|
|
testStr := "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/"
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
isURI(testStr)
|
|
}
|
|
}
|
|
|
|
// TestRunWithInsufficientArgs tests the run function with insufficient command-line arguments.
|
|
func TestRunWithInsufficientArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{
|
|
name: "no arguments",
|
|
args: []string{"articulate-parser"},
|
|
},
|
|
{
|
|
name: "one argument",
|
|
args: []string{"articulate-parser", "source"},
|
|
},
|
|
{
|
|
name: "two arguments",
|
|
args: []string{"articulate-parser", "source", "format"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Run the function
|
|
exitCode := run(tt.args)
|
|
|
|
// Restore stdout
|
|
w.Close()
|
|
os.Stdout = oldStdout
|
|
|
|
// Read captured output
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
output := buf.String()
|
|
|
|
// Verify exit code
|
|
if exitCode != 1 {
|
|
t.Errorf("Expected exit code 1, got %d", exitCode)
|
|
}
|
|
|
|
// Verify usage message is displayed
|
|
if !strings.Contains(output, "Usage:") {
|
|
t.Errorf("Expected usage message in output, got: %s", output)
|
|
}
|
|
|
|
if !strings.Contains(output, "export format") {
|
|
t.Errorf("Expected format information in output, got: %s", output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRunWithHelpFlags tests the run function with help flag arguments.
|
|
func TestRunWithHelpFlags(t *testing.T) {
|
|
helpFlags := []string{"--help", "-h", "help"}
|
|
|
|
for _, flag := range helpFlags {
|
|
t.Run("help_flag_"+flag, func(t *testing.T) {
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Run with help flag
|
|
args := []string{"articulate-parser", flag}
|
|
exitCode := run(args)
|
|
|
|
// Restore stdout
|
|
w.Close()
|
|
os.Stdout = oldStdout
|
|
|
|
// Read captured output
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
output := buf.String()
|
|
|
|
// Verify exit code is 0 (success)
|
|
if exitCode != 0 {
|
|
t.Errorf("Expected exit code 0 for help flag %s, got %d", flag, exitCode)
|
|
}
|
|
|
|
// Verify help content is displayed
|
|
expectedContent := []string{
|
|
"Usage:",
|
|
"source: URI or file path to the course",
|
|
"format: export format",
|
|
"output: output file path",
|
|
"Example:",
|
|
"articulate-sample.json markdown output.md",
|
|
"https://rise.articulate.com/share/xyz docx output.docx",
|
|
}
|
|
|
|
for _, expected := range expectedContent {
|
|
if !strings.Contains(output, expected) {
|
|
t.Errorf("Expected help output to contain %q when using flag %s, got: %s", expected, flag, output)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRunWithVersionFlags tests the run function with version flag arguments.
|
|
func TestRunWithVersionFlags(t *testing.T) {
|
|
versionFlags := []string{"--version", "-v"}
|
|
|
|
for _, flag := range versionFlags {
|
|
t.Run("version_flag_"+flag, func(t *testing.T) {
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Run with version flag
|
|
args := []string{"articulate-parser", flag}
|
|
exitCode := run(args)
|
|
|
|
// Restore stdout
|
|
w.Close()
|
|
os.Stdout = oldStdout
|
|
|
|
// Read captured output
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
output := buf.String()
|
|
|
|
// Verify exit code is 0 (success)
|
|
if exitCode != 0 {
|
|
t.Errorf("Expected exit code 0 for version flag %s, got %d", flag, exitCode)
|
|
}
|
|
|
|
// Verify version content is displayed
|
|
expectedContent := []string{
|
|
"articulate-parser version",
|
|
"Build time:",
|
|
"Git commit:",
|
|
}
|
|
|
|
for _, expected := range expectedContent {
|
|
if !strings.Contains(output, expected) {
|
|
t.Errorf("Expected version output to contain %q when using flag %s, got: %s", expected, flag, output)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRunWithInvalidFile tests the run function with a non-existent file.
|
|
func TestRunWithInvalidFile(t *testing.T) {
|
|
// Capture stdout and stderr
|
|
oldStdout := os.Stdout
|
|
oldStderr := os.Stderr
|
|
|
|
stdoutR, stdoutW, _ := os.Pipe()
|
|
stderrR, stderrW, _ := os.Pipe()
|
|
|
|
os.Stdout = stdoutW
|
|
os.Stderr = stderrW
|
|
|
|
// Also need to redirect log output
|
|
oldLogOutput := log.Writer()
|
|
log.SetOutput(stderrW)
|
|
|
|
// Run with non-existent file
|
|
args := []string{"articulate-parser", "nonexistent-file.json", "markdown", "output.md"}
|
|
exitCode := run(args)
|
|
|
|
// Restore stdout/stderr and log output
|
|
stdoutW.Close()
|
|
stderrW.Close()
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
log.SetOutput(oldLogOutput)
|
|
|
|
// Read captured output
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
io.Copy(&stdoutBuf, stdoutR)
|
|
io.Copy(&stderrBuf, stderrR)
|
|
|
|
stdoutR.Close()
|
|
stderrR.Close()
|
|
|
|
// Verify exit code
|
|
if exitCode != 1 {
|
|
t.Errorf("Expected exit code 1 for non-existent file, got %d", exitCode)
|
|
}
|
|
|
|
// Should have error output
|
|
errorOutput := stderrBuf.String()
|
|
if !strings.Contains(errorOutput, "Error processing course") {
|
|
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
|
}
|
|
}
|
|
|
|
// TestRunWithInvalidURI tests the run function with an invalid URI.
|
|
func TestRunWithInvalidURI(t *testing.T) {
|
|
// Capture stdout and stderr
|
|
oldStdout := os.Stdout
|
|
oldStderr := os.Stderr
|
|
|
|
stdoutR, stdoutW, _ := os.Pipe()
|
|
stderrR, stderrW, _ := os.Pipe()
|
|
|
|
os.Stdout = stdoutW
|
|
os.Stderr = stderrW
|
|
|
|
// Also need to redirect log output
|
|
oldLogOutput := log.Writer()
|
|
log.SetOutput(stderrW)
|
|
|
|
// Run with invalid URI (will fail because we can't actually fetch)
|
|
args := []string{"articulate-parser", "https://example.com/invalid", "markdown", "output.md"}
|
|
exitCode := run(args)
|
|
|
|
// Restore stdout/stderr and log output
|
|
stdoutW.Close()
|
|
stderrW.Close()
|
|
os.Stdout = oldStdout
|
|
os.Stderr = oldStderr
|
|
log.SetOutput(oldLogOutput)
|
|
|
|
// Read captured output
|
|
var stdoutBuf, stderrBuf bytes.Buffer
|
|
io.Copy(&stdoutBuf, stdoutR)
|
|
io.Copy(&stderrBuf, stderrR)
|
|
|
|
stdoutR.Close()
|
|
stderrR.Close()
|
|
|
|
// Should fail because the URI is invalid/unreachable
|
|
if exitCode != 1 {
|
|
t.Errorf("Expected failure (exit code 1) for invalid URI, got %d", exitCode)
|
|
}
|
|
|
|
// Should have error output
|
|
errorOutput := stderrBuf.String()
|
|
if !strings.Contains(errorOutput, "Error processing course") {
|
|
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
|
}
|
|
}
|
|
|
|
// TestRunWithValidJSONFile tests the run function with a valid JSON file.
|
|
func TestRunWithValidJSONFile(t *testing.T) {
|
|
// Create a temporary test JSON file
|
|
testContent := `{
|
|
"title": "Test Course",
|
|
"lessons": [
|
|
{
|
|
"id": "lesson1",
|
|
"title": "Test Lesson",
|
|
"blocks": [
|
|
{
|
|
"type": "text",
|
|
"id": "block1",
|
|
"data": {
|
|
"text": "Test content"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
|
|
tmpFile, err := os.CreateTemp("", "test-course-*.json")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp file: %v", err)
|
|
}
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
if _, err := tmpFile.WriteString(testContent); err != nil {
|
|
t.Fatalf("Failed to write test content: %v", err)
|
|
}
|
|
tmpFile.Close()
|
|
|
|
// Test successful run with valid file
|
|
outputFile := "test-output.md"
|
|
defer os.Remove(outputFile)
|
|
|
|
// Save original stdout
|
|
originalStdout := os.Stdout
|
|
defer func() { os.Stdout = originalStdout }()
|
|
|
|
// Capture stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
args := []string{"articulate-parser", tmpFile.Name(), "markdown", outputFile}
|
|
exitCode := run(args)
|
|
|
|
// Close write end and restore stdout
|
|
w.Close()
|
|
os.Stdout = originalStdout
|
|
|
|
// Read captured output
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
output := buf.String()
|
|
|
|
// Verify successful execution
|
|
if exitCode != 0 {
|
|
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
|
}
|
|
|
|
// Verify success message
|
|
expectedMsg := fmt.Sprintf("Successfully exported course to %s", outputFile)
|
|
if !strings.Contains(output, expectedMsg) {
|
|
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
|
}
|
|
|
|
// Verify output file was created
|
|
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
|
t.Errorf("Expected output file %s to be created", outputFile)
|
|
}
|
|
}
|
|
|
|
// TestRunIntegration tests the run function with different output formats using sample file.
|
|
func TestRunIntegration(t *testing.T) {
|
|
// Skip if sample file doesn't exist
|
|
if _, err := os.Stat("articulate-sample.json"); os.IsNotExist(err) {
|
|
t.Skip("Skipping integration test: articulate-sample.json not found")
|
|
}
|
|
|
|
formats := []struct {
|
|
format string
|
|
output string
|
|
}{
|
|
{"markdown", "test-output.md"},
|
|
{"html", "test-output.html"},
|
|
{"docx", "test-output.docx"},
|
|
}
|
|
|
|
for _, format := range formats {
|
|
t.Run("format_"+format.format, func(t *testing.T) {
|
|
// Capture stdout
|
|
oldStdout := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
// Run the function
|
|
args := []string{"articulate-parser", "articulate-sample.json", format.format, format.output}
|
|
exitCode := run(args)
|
|
|
|
// Restore stdout
|
|
w.Close()
|
|
os.Stdout = oldStdout
|
|
|
|
// Read captured output
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
output := buf.String()
|
|
|
|
// Clean up test file
|
|
defer os.Remove(format.output)
|
|
|
|
// Verify successful execution
|
|
if exitCode != 0 {
|
|
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
|
}
|
|
|
|
// Verify success message
|
|
expectedMsg := "Successfully exported course to " + format.output
|
|
if !strings.Contains(output, expectedMsg) {
|
|
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
|
}
|
|
|
|
// Verify output file was created
|
|
if _, err := os.Stat(format.output); os.IsNotExist(err) {
|
|
t.Errorf("Expected output file %s to be created", format.output)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMainFunction tests that the main function exists and is properly structured.
|
|
// We can't test os.Exit behavior directly, but we can verify the main function
|
|
// calls the run function correctly by testing run function behavior.
|
|
func TestMainFunction(t *testing.T) {
|
|
// Test that insufficient args return exit code 1
|
|
exitCode := run([]string{"program"})
|
|
if exitCode != 1 {
|
|
t.Errorf("Expected run to return exit code 1 for insufficient args, got %d", exitCode)
|
|
}
|
|
|
|
// Test that main function exists (this is mainly for coverage)
|
|
// The main function just calls os.Exit(run(os.Args)), which we can't test directly
|
|
// but we've tested the run function thoroughly above.
|
|
}
|